From 1453fb16ed366e391f03dfa8d037206f66c7a651 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 9 Sep 2026 14:43:25 +0800 Subject: [PATCH] fix(rectification): keep single-event opening on the existing collect path (BUG-604) Co-authored-by: Cursor --- CHANGELOG.md | 2 +- ...ification-conversation-economy-20260909.md | 18 ++++--- ...ification-conversation-economy-20260909.md | 3 +- .../rectification-scenarios-20260907.md | 3 +- frontend/docs/VOICE.md | 1 + .../lib/rectification-agentic/user-copy.ts | 11 +++++ .../v9/method-followup.ts | 49 +++++++++++++++++-- .../tests/agent-voice-copy-contract.test.ts | 2 + .../tests/rectification-answer-choice.test.ts | 11 +++-- ...ication-collect-direction-20260904.test.ts | 19 ++++--- .../tests/rectification-collect-stall.test.ts | 6 ++- .../rectification-decision-authority.test.ts | 5 +- .../tests/rectification-eight-method.test.ts | 7 ++- ...ication-post-adopt-verify-20260904.test.ts | 10 +++- .../rectification-spoken-collect.test.ts | 29 +++++++++++ frontend/tests/skill-registry.test.ts | 2 +- .../jyotish-birth-time-rectification/SKILL.md | 2 +- .../references/conversation-strategy.md | 2 +- .../versions/10.0.19/SKILL.md | 2 +- .../references/conversation-strategy.md | 2 +- skills/skill-package-registry.json | 2 +- 21 files changed, 150 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9719f074..34c09437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2026-09-09 — 生时校正开场一次可报多件,采集题可点「没有 / 记不清」,每轮只留一句话 -开场先讲当前搜索窗口和核对做法,说明最后给区间和代表分钟、不给精确到秒,并请人想到几件说几件;会点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤,仍不写具体年份、不要求先准备材料。一条消息里报三件只跑一次比较。口述采集题输入框上方出现「没有」「记不清」两个按钮:「没有」记下并跳过这方面,「记不清」记下这题先放着,本会话不再问该领域,采用后核对仍可能碰到。助手气泡不再写「本轮对照了…」方法句,那句改进行程记录的展开区;点选后旁白只说范围没变或收到哪一段;记下经历时只复述年-月和事件,不评价「很有帮助」。Skill 版本 10.0.19。 +开场先讲当前搜索窗口和核对做法,说明最后给区间和代表分钟、不给精确到秒,并请人想到几件说几件;会点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤,仍不写具体年份、不要求先准备材料。一条消息里报三件只跑一次比较。只报一件也按原来的逐领域采集往下走,不会追问「还有吗」;第一道逐领域题干末尾会加一次「想到别的也可以一起说」。口述采集题输入框上方出现「没有」「记不清」两个按钮:「没有」记下并跳过这方面,「记不清」记下这题先放着,本会话不再问该领域,采用后核对仍可能碰到。助手气泡不再写「本轮对照了…」方法句,那句改进行程记录的展开区;点选后旁白只说范围没变或收到哪一段;记下经历时只复述年-月和事件,不评价「很有帮助」。Skill 版本 10.0.19。 ## 2026-09-09 — 生时校正对话上方常驻一条时间轴,看得见范围在收窄 diff --git a/docs/tasks/PROGRESS-rectification-conversation-economy-20260909.md b/docs/tasks/PROGRESS-rectification-conversation-economy-20260909.md index 48050dff..d469bee5 100644 --- a/docs/tasks/PROGRESS-rectification-conversation-economy-20260909.md +++ b/docs/tasks/PROGRESS-rectification-conversation-economy-20260909.md @@ -5,13 +5,14 @@ 任务书:`docs/tasks/TASK-rectification-conversation-economy-20260909.md` 基线:`origin/staging` @ `d9252516`(任务书写 aa499da1;本 worktree 从当时的 staging 头切出,含时间轴文档提交) -本单状态:**待验收**。产品提交 `aa7ccb30`;尚未 push,未合入 staging/main。 +本单状态:**待验收**。产品提交 `aa7ccb30`;单事件开场补丁见本文件后文 SHA。尚未 push,未合入 staging/main。未合时间轴兄弟单。 -未改:采用门、确认门、各道门槛、`page.tsx`、`use-conversation-scroll-anchor.ts`。未改时间轴兄弟单文件(`rectification-timeline-scale.ts` / `rectification-candidate-result.ts` / 时间轴测试 / DESIGN §10)。Skill 从 10.0.18 升到 **10.0.19**。 +未改:采用门、确认门、各道门槛、`page.tsx`、`use-conversation-scroll-anchor.ts`。未改时间轴兄弟单文件(`rectification-timeline-scale.ts` / `rectification-candidate-result.ts` / 时间轴测试 / DESIGN §10)。Skill 停在 **10.0.19**(live + 快照同步改 OpeningPolicy,不升版)。 ## 做了什么 - **BUG-604 / 决策 1** `buildOpeningBrief` 写入当前搜索窗口(`candidate_range` HH:MM–HH:MM)与 intake 来源、做法三句、六类领域。开场正文不合格则换成 `openingSpokenBody`。首题仍是 `collect:other:*`,题干 `GENERIC_COLLECT_QUESTION` =「先说你最容易想起的一两件,年月大概就行。」不写具体年份、不要求先准备材料。批量写入仍走 `rectification-record-evidence-batch`;已确认领域不再被 `nextDatedCollectFollowup` 追问。 +- **BUG-604 追问补丁(`origin/staging` `54adb0d1`)** 邀请多件是降低门槛,不是要求。开场只报一件:batch 写一条、compare 一次、下一问仍是既有 `nextDatedCollectFollowup`(顺序不变)。不插「还有吗 / 还能想起别的吗」追问轮,也不重复开场邀请。第一道逐领域题干由服务端加一次 `FIRST_DATED_COLLECT_INVITE` =「想到别的也可以一起说。」(`invite_more_once`,只一次)。用户可见文案未出现「还有吗」。 - **BUG-605 / 决策 2** `collect_spoken` 且未 busy 时,`composer-wrap` 内两个 44px 次要按钮,类名 `rectification-collect-reply`(不复用 `rectification-step-state`,无示例骨架条)。「没有」= 打字「没有」= declined,回执「记下了,这方面先跳过。」「记不清」= skipped,回执「记下了,这题先放着。」精确这两句不调模型。skipped 本会话不再采集;采用后 `remainingReverseVerifyProbes` 仍可碰到 skipped(`includeSkipped: false`)。 - **BUG-606 / 决策 3** 气泡不再渲染 `vargaSentence`;展开的 `ConsultationRunTimeline` / `AgentActivityStatus` 保留。点选旁白只有「已记录,范围没变。」或「已记录,范围收到 / 变为 A–B。」证据轮 `stripQuestionSentences` 之后 `trimEvidenceTurnBody`:超过两句只留首句,去掉「很有帮助 / 很有价值 / 很有分量 / 特别有用」。 @@ -32,16 +33,19 @@ | 证据轮正文 | 2–4 句额度;模型三句原样落库 | 超过两句只留首句;去掉价值评价四词 | BUG-606(c) | | Agent 提示 | 「正文只做承接(2-4 句)」;「接下来我们继续」 | 「正文只做承接」+「证据轮正文只写一句复述」;题干作下一段 | BUG-606 | | 流式三句证据 | `answerText` 等于模型全文 | 落库首句,并以 `replace: true` 覆盖可见正文 | BUG-606 服务端裁剪 | +| 只报一件后的下一问 | 未单独锁;有插追问轮风险 | 下一问 = 既有 dated collect(education 后是 family);正文无「还有吗 / 别的吗」 | `54adb0d1`;邀请不是要求 | +| 第一道逐领域题干 | 等于 `USER_COLLECT_QUESTION.*` | 末尾一次接「想到别的也可以一起说。」;第二道起不再接 | 不是独立追问轮 | +| Skill 10.0.19 哈希 | `a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b` | `a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8` | OpeningPolicy 补单事件口径;版本不升 | ## 测试 - `frontend` `./node_modules/.bin/tsc --noEmit`:exit 0 - `npm run lint`:0 errors / 108 warnings(仓库基线 warning,无新 error) -- 任务书指定切片:`tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` + `skill-registry.test.ts`(排除 database):**1065 tests / 1065 pass / 0 fail** +- 任务书指定切片:`tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` + `skill-registry.test.ts`(排除 database):**1066 tests / 1066 pass / 0 fail**(含新增「single confirmed domain opening goes to dated collect without a chase-more turn」) - Docker `database-rectification-*.test.ts`:本机未跑,记 blocked -- 浏览器真人走查:browser MCP 可用但无打开的页、无登录态的 `collect_spoken` 会话,未点到「没有 / 记不清」。采集按钮与 44px、不复用 `rectification-step-state` 由 `rectification-spoken-collect.test.ts` 锁源码。 +- 浏览器真人走查:browser MCP 可用但无打开的页、无登录态的 `collect_spoken` 会话,未点到「没有 / 记不清」。采集按钮与 44px、不复用 `rectification-step-state` 由 `rectification-spoken-collect.test.ts` 锁源码。本补丁只改题干后缀与路由,无新 UI。 -Skill 10.0.19 SHA256:`a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b`。live 树与 `versions/10.0.19` 一致,registry 哈希与 `computeSkillPackageSha256` 一致。 +Skill 10.0.19 SHA256:`a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8`。live 树与 `versions/10.0.19` 一致,registry 哈希与 `computeSkillPackageSha256` 一致。 ## 偏离 / blocked @@ -49,6 +53,6 @@ Skill 10.0.19 SHA256:`a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e3 2. 本机主仓停在 `codex/minute-rectification-p0` 且有脏文件;未 checkout / reset / stash。本单只在本 worktree。 3. `move_agent_to_root` 对子代理不可用;全程用本 worktree 绝对路径。 4. `.venv` 是指向主仓的 symlink,不提交。 -5. `origin/staging` 在切出后超前 2 个提交;按任务书不 merge、不 push。 -6. BUG_HISTORY BUG-604/605/606 修复版本:`aa7ccb30`(产品提交;本条为回填 SHA 的文档提交,不 amend)。 +5. `origin/staging` 在切出后超前(含 `54adb0d1` 任务书补丁、`d5972ef4` BUG-607)。只把 `54adb0d1` 的 §2.1 单事件口径摘进本 TASK;**不**取 BUG-607,不 merge staging,不 push。 +6. BUG_HISTORY BUG-604/605/606 修复版本:`aa7ccb30`(产品提交)。单事件开场补丁是新提交,不 amend。 7. 点选旁白测试里,若下一问未能单独落库,服务端仍可能把采集题干接在「已记录,范围收到…」后面。断言改为前缀匹配,不把下一问挂载当成旁白模板的一部分。 diff --git a/docs/tasks/TASK-rectification-conversation-economy-20260909.md b/docs/tasks/TASK-rectification-conversation-economy-20260909.md index d2552e11..f8abbedf 100644 --- a/docs/tasks/TASK-rectification-conversation-economy-20260909.md +++ b/docs/tasks/TASK-rectification-conversation-economy-20260909.md @@ -19,7 +19,8 @@ ### 2.1 BUG-604 开场(决策 1) - `buildOpeningBrief` 改为给模型三个事实:当前搜索窗口(`candidate_range`,如 04:45–05:15)与来源(intake 声明的不确定档)、做法三句的要点、六类领域清单(升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤)。正文模板由 Skill 规定为:一句窗口与做法、一句"最后给区间和代表分钟,不给精确到秒"、一句"想到几件说几件,有大概年月就行"+ 六类;仍不得写具体年份、不得要求先准备材料。首题 `spokenPrompt` 保持采集题身份(`collect:other:*`),题干改为"先说你最容易想起的一两件,年月大概就行"。 - 批量写入:`rectification-record-evidence-batch` 已支持多事件;确认一条消息含 3 件不同领域事件时 batch 一次写入、compare 只跑一次、后续逐领域采集只问缺口(`nextDatedCollectFollowup` 已按 `hasConfirmedDomain` 跳过)。 -- 验收:opening 落库正文 ≤ 4 句、含六类中至少五类、不含年份;mock 一条三事件消息 → receipt 里 compare 一次、`evidence` 三条、下一问不再是这三个领域;`agent-voice-copy-contract` 收新句。SKILL.md L52 改口径,`conversation-strategy.md` OpeningPolicy 同步,Skill 10.0.19 快照与 registry。 +- **只报一件也必须走通(产品 2026-09-09 追问)。** 邀请是降低门槛,不是要求。用户开场只说一件事时,流程与今天完全一致:batch 写一条、compare 一次、下一问是既有的逐领域采集题(`nextDatedCollectFollowup` 顺序不变);**不得**插入"还有吗 / 还能想起别的吗"这种追问轮,也不得因为只报了一件就重复开场邀请。逐领域采集题的题干末尾可以带一次"想到别的也可以一起说"(服务端固定句,只在第一道逐领域采集题上出现一次)。 +- 验收:opening 落库正文 ≤ 4 句、含六类中至少五类、不含年份;mock 一条单事件消息 → compare 一次、下一问为逐领域采集题、正文不含"还有吗 / 别的吗";mock 一条三事件消息 → receipt 里 compare 一次、`evidence` 三条、下一问不再是这三个领域;`agent-voice-copy-contract` 收新句。SKILL.md L52 改口径,`conversation-strategy.md` OpeningPolicy 同步,Skill 10.0.19 快照与 registry。 ### 2.2 BUG-605 「没有 / 记不清」(决策 2) - `collect_spoken` 焦点激活且未 busy 时,`composer-wrap` 内输入框上方渲染两个按钮(44px,次要样式,与被删的「先这样」不同类名,不得复用 `rectification-step-state`)。「没有」POST 与打字"没有"相同的请求(复用 `applyRectificationChoice`/拒答分支,`collectDeclinedAck` 原句);「记不清」走 `resolve-focus status=skipped` 的服务端路径,回执"记下了,这题先放着。"(新文案对照 VOICE.md)。 diff --git a/docs/testing/rectification-scenarios-20260907.md b/docs/testing/rectification-scenarios-20260907.md index 9b0b0ef6..45cc9e63 100644 --- a/docs/testing/rectification-scenarios-20260907.md +++ b/docs/testing/rectification-scenarios-20260907.md @@ -158,13 +158,14 @@ 资料:家人记得大概时间,钟点任意,范围「差不多准」。地点任意公开城市。虚构经历。 -开场后用一条消息说三件不同领域、带年月的事(例如上学、第一份工作、搬家)。之后继续采集。 +开场后用一条消息说三件不同领域、带年月的事(例如上学、第一份工作、搬家)。另走一遍只说一件。之后继续采集。 期望: - 开场正文不超过四句,点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤中至少五类,不含具体年份 - 这一条消息只触发一次候选比较,账本写入三件 - 下一问不再是已经说过的那三个领域 +- 只报一件时也只比较一次,下一问是既有逐领域采集,正文没有「还有吗 / 还能想起别的吗」;第一道逐领域题干末尾可以有一次「想到别的也可以一起说」 - 口述采集输入框上方有「没有」「记不清」,没有「先这样」 - 每轮助手正文一句复述(可另接一句范围变化);气泡里没有「本轮对照了…」,也没有「很有帮助 / 很有价值 / 很有分量 / 特别有用」 diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index 5e6b3d49..8ec79964 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -46,6 +46,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 | 停止后出现红色告警条 | 已停止,已生成的内容保留;本次不会扣点。 | 停止是用户动作,不是出错。 | | 也可以再说一件你记得大概时间的事。 | 能分开候选的问题已经问完,先按现有材料给你结果。 / 还差带月份的经历,领域不限。 | 采集问完后交付或说清门槛,不再开放采集。 | | 口述采集没有停止按钮,范围小字不可点 | 选择题卡内有「先这样,先看当前范围」;生成中有「停止回答」。口述采集输入框上方是「没有」「记不清」两个 44px 次要按钮,不是先这样。范围小字是状态句,不可点。 | 没有=拒答,记不清=本会话先放着。 | +| 只报一件后追问「还有吗,还能想起别的吗」 | 下一问仍是既有逐领域采集。第一道题干末尾由服务端加一次「想到别的也可以一起说。」 | 邀请不是要求;不插追问轮,也不重复开场。 | | 点选后旁白写「04:31–04:39 这段领先,05:00–05:07 这段落后」 | 已记录,范围收到 04:31–04:39。 | 旁白只说范围变没变。领先/落后留在卡内「答了会怎样」,不进气泡。 | | 每轮只有技法清单,不知道现在第几步 | 输入框上方有「第 N 步 · 原因 · 下一步」。 | 用户始终看得见该做什么。 | | 完全不清楚出生时间后劝退「生时校正以后需要时再做」 | 可以直接开始生时校正:先从你记得的经历比出大致时段。 | 不知道钟点时先比时段,不劝退、不给分钟卡。 | diff --git a/frontend/src/lib/rectification-agentic/user-copy.ts b/frontend/src/lib/rectification-agentic/user-copy.ts index 3883bdf2..8307b7b8 100644 --- a/frontend/src/lib/rectification-agentic/user-copy.ts +++ b/frontend/src/lib/rectification-agentic/user-copy.ts @@ -30,6 +30,15 @@ export type CollectDomain = export const GENERIC_COLLECT_QUESTION = "先说你最容易想起的一两件,年月大概就行。"; +/** One-time suffix on the first dated-collect stem. Not a chase-more turn. */ +export const FIRST_DATED_COLLECT_INVITE = "想到别的也可以一起说。"; + +export function withFirstDatedCollectInvite(stem: string): string { + const spoken = stem.trim(); + if (!spoken || spoken.includes(FIRST_DATED_COLLECT_INVITE)) return spoken; + return `${spoken}${FIRST_DATED_COLLECT_INVITE}`; +} + /** Six everyday domains named in the opening body. Not years. */ export const OPENING_COLLECT_DOMAINS = [ "升学", @@ -108,6 +117,7 @@ export const RECTIFICATION_USER_COPY = { collectHandoff: "接下来我们继续。", collectDeclinedAck: "记下了,这方面先跳过。", collectSkippedAck: "记下了,这题先放着。", + firstDatedCollectInvite: FIRST_DATED_COLLECT_INVITE, uncertaintyStop: "前面几道题你多半选了\"说不好\",再问下去也分不开,先停在这里。", tiedFirstStop: "几个候选打成平手,问题已经分不开它们。", probePoolExhaustedStop: "能分开候选的问题已经问完,先按现有材料给你结果。", @@ -428,6 +438,7 @@ export function listUserVisibleCopy(): string[] { RECTIFICATION_USER_COPY.collectHandoff, RECTIFICATION_USER_COPY.collectDeclinedAck, RECTIFICATION_USER_COPY.collectSkippedAck, + FIRST_DATED_COLLECT_INVITE, GENERIC_COLLECT_QUESTION, openingSpokenBody(["04:45", "05:15"]), RECTIFICATION_USER_COPY.uncertaintyStop, diff --git a/frontend/src/lib/rectification-agentic/v9/method-followup.ts b/frontend/src/lib/rectification-agentic/v9/method-followup.ts index a391368c..07cb491b 100644 --- a/frontend/src/lib/rectification-agentic/v9/method-followup.ts +++ b/frontend/src/lib/rectification-agentic/v9/method-followup.ts @@ -84,6 +84,7 @@ import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, USER_COLLECT_QUESTION_RETRY, + withFirstDatedCollectInvite, } from "../user-copy.ts"; export { GENERIC_COLLECT_QUESTION }; @@ -194,6 +195,7 @@ export type MethodFollowup = Readonly<{ selection_score?: number; probe_id?: string; collect_retry?: boolean; + invite_more_once?: boolean; block_periods?: Readonly>; widen_windows?: Readonly>; date_reliability_evidence_id?: string; @@ -1211,7 +1213,12 @@ export function spokenFollowupForUser( const mapped = followup.collect_retry === true ? (USER_COLLECT_QUESTION_RETRY[domain] ?? USER_COLLECT_QUESTION[domain]) : USER_COLLECT_QUESTION[domain]; - return mapped ?? null; + if (!mapped) return null; + const dated = (DATED_COLLECT_ORDER as readonly string[]).includes(domain); + if (followup.invite_more_once && dated && followup.collect_retry !== true) { + return withFirstDatedCollectInvite(mapped); + } + return mapped; } export const DATED_COLLECT_ORDER = [ @@ -1260,15 +1267,29 @@ function datedCollectDomainBlocked( || answeredYes.has(domain); } +export function datedCollectAlreadyAsked( + declined: ReadonlySet, + topics: readonly Readonly>[] = [], +): boolean { + if (DATED_COLLECT_ORDER.some((domain) => declined.has(domain))) return true; + return DATED_COLLECT_ORDER.some((domain) => domainCollectFocusAsked(topics, domain)); +} + export function nextDatedCollectFollowup( evidence: readonly MethodFollowupEvidence[], declined: ReadonlySet, answeredYes: ReadonlySet = new Set(), + options?: { askedDatedCollect?: boolean }, ): MethodFollowup | null { for (const domain of DATED_COLLECT_ORDER) { if (datedCollectDomainBlocked(domain, evidence, declined, answeredYes)) continue; const next = datedCollectFollowup(domain, evidence); - if (next) return next; + if (next) { + return { + ...next, + invite_more_once: options?.askedDatedCollect !== true, + }; + } } return null; } @@ -1357,7 +1378,9 @@ export function exhaustionSpokenCollectFollowup(input: { }): MethodFollowup | null { const declined = declinedDomains(input.declinedTopics ?? []); const answeredYes = domainsAnsweredYes(input.answeredProbes, input.eventProbes); - const dated = nextDatedCollectFollowup(input.evidence, declined, answeredYes); + const dated = nextDatedCollectFollowup(input.evidence, declined, answeredYes, { + askedDatedCollect: datedCollectAlreadyAsked(declined, input.declinedTopics ?? []), + }); if (dated) return dated; if ( !declined.has("occupation") @@ -1883,9 +1906,15 @@ export function buildMethodFollowupPlan(input: { const collectRetry = base.intent === "collect_method_evidence" && typeof base.domain === "string" && domainCollectFocusAsked(askedRows, base.domain); + const inviteMoreOnce = base.intent === "collect_method_evidence" + && typeof base.domain === "string" + && (DATED_COLLECT_ORDER as readonly string[]).includes(base.domain) + && !collectRetry + && !datedCollectAlreadyAsked(declinedDomains(input.declinedTopics ?? []), askedRows); return { ...base, ...(collectRetry ? { collect_retry: true } : {}), + ...(inviteMoreOnce ? { invite_more_once: true } : { invite_more_once: false }), choice_frame: attach ? buildChoiceFrame(base, { observations: input.observations, @@ -2512,7 +2541,12 @@ export function buildMethodFollowupPlan(input: { && precisionCard?.choice_frame ) { next = precisionCard; - } else if ((datedCollect = nextDatedCollectFollowup(input.evidence, declined, answeredYes))) { + } else if ((datedCollect = nextDatedCollectFollowup(input.evidence, declined, answeredYes, { + askedDatedCollect: datedCollectAlreadyAsked(declined, [ + ...(input.closedCollectFocuses ?? []), + ...(input.declinedTopics ?? []), + ]), + }))) { next = makeFollowup(datedCollect); } else if (!occupationCovered) { if (meetsAcceptanceEventQuality(input.evidence)) { @@ -2688,7 +2722,12 @@ export function buildMethodFollowupPlan(input: { next = null; } if (!next) { - const leftoverDated = nextDatedCollectFollowup(input.evidence, declined, answeredYes); + const leftoverDated = nextDatedCollectFollowup(input.evidence, declined, answeredYes, { + askedDatedCollect: datedCollectAlreadyAsked(declined, [ + ...(input.closedCollectFocuses ?? []), + ...(input.declinedTopics ?? []), + ]), + }); if (leftoverDated) { next = makeFollowup(leftoverDated); } else if (!occupationCovered) { diff --git a/frontend/tests/agent-voice-copy-contract.test.ts b/frontend/tests/agent-voice-copy-contract.test.ts index 3291ca05..5934c4ee 100644 --- a/frontend/tests/agent-voice-copy-contract.test.ts +++ b/frontend/tests/agent-voice-copy-contract.test.ts @@ -275,6 +275,8 @@ test("opening body lists domains without years and the stem no longer lists year assert.equal(GENERIC_COLLECT_QUESTION, "先说你最容易想起的一两件,年月大概就行。"); assert.doesNotMatch(GENERIC_COLLECT_QUESTION, /比如/); assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.collectSkippedAck)); + assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.firstDatedCollectInvite)); + assert.doesNotMatch(listUserVisibleCopy().join("\n"), /还有吗|还能想起别的吗/); assert.ok(MACHINE_VOICE_LEXICON.includes("领先")); assert.ok(MACHINE_VOICE_LEXICON.includes("落后")); }); diff --git a/frontend/tests/rectification-answer-choice.test.ts b/frontend/tests/rectification-answer-choice.test.ts index 120381fa..fee06475 100644 --- a/frontend/tests/rectification-answer-choice.test.ts +++ b/frontend/tests/rectification-answer-choice.test.ts @@ -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 } from "../src/lib/rectification-agentic/user-copy.ts"; +import { containsBoundarySemantics, RECTIFICATION_USER_COPY, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts"; import { evidenceLedgerFingerprint, parseV9CaseDossier, @@ -978,8 +978,13 @@ 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 含 2021 → 新:口语等于 USER_COLLECT_QUESTION.family → 决策 7 采集题不带年份前缀 - assert.equal(schema.prompt, RECTIFICATION_USER_COPY.collectQuestionByDomain.family); + // 旧:prompt 等于 USER_COLLECT_QUESTION.family + // 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」 + // 原因:BUG-604;年份仍不进采集题干 + assert.equal( + schema.prompt, + withFirstDatedCollectInvite(RECTIFICATION_USER_COPY.collectQuestionByDomain.family), + ); assert.match(schema.prompt ?? "", /家人|结婚|添丁|住院/); assert.equal(applied.nextInterviewPersisted, true); assert.equal(applied.nextChoiceReady, false); diff --git a/frontend/tests/rectification-collect-direction-20260904.test.ts b/frontend/tests/rectification-collect-direction-20260904.test.ts index dbf376f4..2678227d 100644 --- a/frontend/tests/rectification-collect-direction-20260904.test.ts +++ b/frontend/tests/rectification-collect-direction-20260904.test.ts @@ -28,7 +28,7 @@ import { parseV9CaseDossier, RectificationToolServiceError, } from "../src/lib/rectification-agentic/v9/tool-service.ts"; -import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts"; +import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, 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 { @@ -186,7 +186,10 @@ 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); - assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family); + // 旧:口语等于 USER_COLLECT_QUESTION.family + // 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」 + // 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮 + assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family)); }); test("four scoreable events skip holdout for domains already in the ledger", () => { @@ -582,13 +585,13 @@ test("career discriminator yes covers career collect without ledger career evide }); assert.equal(unanswered.next_followup?.domain, "career"); assert.equal(unanswered.next_followup?.intent, "collect_method_evidence"); - assert.equal(spokenFollowupForUser(unanswered.next_followup), USER_COLLECT_QUESTION.career); + assert.equal(spokenFollowupForUser(unanswered.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career)); const yes = careerAnswerPlan("yes"); assert.notEqual(yes.next_followup?.domain, "career"); assert.equal(yes.next_followup?.domain, "education"); assert.equal(yes.next_followup?.intent, "collect_method_evidence"); - assert.equal(spokenFollowupForUser(yes.next_followup), USER_COLLECT_QUESTION.education); + assert.equal(spokenFollowupForUser(yes.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.education)); assert.equal( yes.methods.find((item) => item.method_id === "d10_career")?.status, "covered", @@ -600,17 +603,17 @@ test("career discriminator yes covers career collect without ledger career evide const no = careerAnswerPlan("no"); assert.equal(no.next_followup?.domain, "career"); - assert.equal(spokenFollowupForUser(no.next_followup), USER_COLLECT_QUESTION.career); + assert.equal(spokenFollowupForUser(no.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career)); const unsure = careerAnswerPlan("unsure"); assert.equal(unsure.next_followup?.domain, "career"); - assert.equal(spokenFollowupForUser(unsure.next_followup), USER_COLLECT_QUESTION.career); + assert.equal(spokenFollowupForUser(unsure.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career)); }); test("career discriminator yes does not invent coverage from a semantic_key prefix", () => { const plan = careerAnswerPlan("yes", { eventProbes: [] }); assert.equal(plan.next_followup?.domain, "career"); - assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.career); + assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career)); }); test("ledger-classified career probe answers do not cover career collect", () => { @@ -623,7 +626,7 @@ test("ledger-classified career probe answers do not cover career collect", () => }], }); assert.equal(plan.next_followup?.domain, "career"); - assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.career); + assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career)); }); const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883"; diff --git a/frontend/tests/rectification-collect-stall.test.ts b/frontend/tests/rectification-collect-stall.test.ts index 094d19cb..b435ec94 100644 --- a/frontend/tests/rectification-collect-stall.test.ts +++ b/frontend/tests/rectification-collect-stall.test.ts @@ -33,6 +33,7 @@ import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9 import { RECTIFICATION_USER_COPY, USER_COLLECT_QUESTION, + withFirstDatedCollectInvite, } from "../src/lib/rectification-agentic/user-copy.ts"; import { ATTEMPT_ID, @@ -486,7 +487,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); - assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family); + // 旧:口语等于 USER_COLLECT_QUESTION.family + // 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」 + // 原因:BUG-604 单事件开场不插追问轮 + assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family)); assert.equal(plan.dropped_probes.some((item) => ( item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast" )), true); diff --git a/frontend/tests/rectification-decision-authority.test.ts b/frontend/tests/rectification-decision-authority.test.ts index 265c47c2..4f9d45b8 100644 --- a/frontend/tests/rectification-decision-authority.test.ts +++ b/frontend/tests/rectification-decision-authority.test.ts @@ -673,7 +673,10 @@ test("nonterminal invariant 3: answer_choice response waits until incident fallb // 保留语义:answer_choice 出口仍必须先把家人采集题干写入 focus/turn,再返回 HTTP。 assert.equal(decision.canAdopt, true); assert.ok(currentQuestion?.prompt); - assert.equal(currentQuestion.prompt, "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。"); + // 旧:家人采集题干原句 + // 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」 + // 原因:BUG-604 单事件开场不插追问轮 + assert.equal(currentQuestion.prompt, "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。想到别的也可以一起说。"); const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus"); assert.ok(setFocus); assert.equal(setFocus.args.p_intent, "collect_method_evidence"); diff --git a/frontend/tests/rectification-eight-method.test.ts b/frontend/tests/rectification-eight-method.test.ts index c1179226..d015305b 100644 --- a/frontend/tests/rectification-eight-method.test.ts +++ b/frontend/tests/rectification-eight-method.test.ts @@ -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 } from "../src/lib/rectification-agentic/user-copy.ts"; +import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, 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"; @@ -2381,7 +2381,10 @@ test("family collect attaches the collection-probe year instead of a yearless D2 // 原因:决策 7,年份只用于计分与去重,不进采集题干 assert.equal(plan.next_followup?.year_label, undefined); assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/); - assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family); + // 旧:口语等于 USER_COLLECT_QUESTION.family + // 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」 + // 原因:BUG-604 单事件开场不插追问轮 + assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family)); }); test("already-open yearless family card is abandoned instead of kept as a scoring frame", () => { diff --git a/frontend/tests/rectification-post-adopt-verify-20260904.test.ts b/frontend/tests/rectification-post-adopt-verify-20260904.test.ts index 47cca079..eb0a9858 100644 --- a/frontend/tests/rectification-post-adopt-verify-20260904.test.ts +++ b/frontend/tests/rectification-post-adopt-verify-20260904.test.ts @@ -11,7 +11,10 @@ import { buildNextUserAction, spokenFollowupForUser, } from "../src/lib/rectification-agentic/v9/method-followup.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 { RECTIFICATION_QUESTION_RETRY_LIMIT, rectificationQuestionGapState, @@ -204,6 +207,9 @@ 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); - assert.equal(spoken, USER_COLLECT_QUESTION.family); + // 旧:口语等于 USER_COLLECT_QUESTION.family + // 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」 + // 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮 + assert.equal(spoken, withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family)); assert.doesNotMatch(spoken ?? "", /^\d{4} 年前后,/); }); diff --git a/frontend/tests/rectification-spoken-collect.test.ts b/frontend/tests/rectification-spoken-collect.test.ts index 7780dbc7..b78a9042 100644 --- a/frontend/tests/rectification-spoken-collect.test.ts +++ b/frontend/tests/rectification-spoken-collect.test.ts @@ -14,6 +14,7 @@ import { declinedCollectDomains, nextDatedCollectFollowup, reverseVerifyRemainingForAdopt, + spokenFollowupForUser, spokenCollectFallbackFollowup, type MethodFollowup, type MethodFollowupEvidence, @@ -482,6 +483,34 @@ test("skipped collect domains are not re-asked this session; reverse-verify may assert.equal(remaining.some((probe) => probe.domain === "family"), true); }); +test("single confirmed domain opening goes to dated collect without a chase-more turn", () => { + const evidence: MethodFollowupEvidence[] = [ + { status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null }, + ]; + const next = nextDatedCollectFollowup(evidence, new Set()); + assert.ok(next); + assert.notEqual(next.domain, "education"); + assert.equal(next.domain, "family"); + const spoken = spokenFollowupForUser(next); + assert.ok(spoken); + assert.match(spoken, /家里/); + assert.match(spoken, /想到别的也可以一起说/); + assert.doesNotMatch(spoken, /还有吗|别的吗|还能想起别的吗/); + assert.doesNotMatch(spoken, /先说你最容易想起的一两件/); + const later = nextDatedCollectFollowup(evidence, new Set(), new Set(), { askedDatedCollect: true }); + assert.equal(later?.domain, "family"); + assert.doesNotMatch(spokenFollowupForUser(later) ?? "", /想到别的也可以一起说/); + const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8"); + assert.match(tools, /recordV10EvidenceBatch/); + assert.match(tools, /result\.acceptedCount > 0[\s\S]*autoRescoreAfterEvidenceChange/); + const copy = readFileSync(new URL("../src/lib/rectification-agentic/user-copy.ts", import.meta.url), "utf8"); + const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8"); + const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8"); + assert.doesNotMatch(copy, /还有吗|还能想起别的吗/); + assert.doesNotMatch(agent, /还有吗|还能想起别的吗/); + assert.doesNotMatch(agentRun, /还有吗|还能想起别的吗/); +}); + test("three confirmed domains from one message are not re-asked by dated collect", () => { const evidence: MethodFollowupEvidence[] = [ { status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null }, diff --git a/frontend/tests/skill-registry.test.ts b/frontend/tests/skill-registry.test.ts index c4500068..0c6a203e 100644 --- a/frontend/tests/skill-registry.test.ts +++ b/frontend/tests/skill-registry.test.ts @@ -86,7 +86,7 @@ test("checked-in registry verifies hashed product packages and leaves consult on { name: "jyotish-birth-time-rectification", version: "10.0.19", - sha256: "a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b", + sha256: "a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8", }, { name: "jyotish-personal-report", diff --git a/skills/jyotish-birth-time-rectification/SKILL.md b/skills/jyotish-birth-time-rectification/SKILL.md index 8455ba34..0a915db3 100644 --- a/skills/jyotish-birth-time-rectification/SKILL.md +++ b/skills/jyotish-birth-time-rectification/SKILL.md @@ -57,7 +57,7 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv 开场必须满足: -- 一条消息可以报多件;想到几件说几件,有大概年月即可。 +- 一条消息可以报多件;想到几件说几件,有大概年月即可。只报一件也继续既有逐领域采集,不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。 - 允许模糊日期:可以先说大概年份、阶段或范围;如确有信息增益,后续再澄清,不诱导猜测月份或日期。 - 首题保持采集题身份(`collect:other:*`),题干写成「先说你最容易想起的一两件,年月大概就行」。 - 至多一个主问题且唯一来源:每轮当前问题只能由服务端建立 `ConversationFocus` 并通过界面问题槽呈现。Agent 回复正文只做承接与解释,不得自行提出、复述、改写或预告问题;正文内容不参与问题槽判定。 diff --git a/skills/jyotish-birth-time-rectification/references/conversation-strategy.md b/skills/jyotish-birth-time-rectification/references/conversation-strategy.md index cd2b7f32..49de1898 100644 --- a/skills/jyotish-birth-time-rectification/references/conversation-strategy.md +++ b/skills/jyotish-birth-time-rectification/references/conversation-strategy.md @@ -18,7 +18,7 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什 首次开场只使用服务器 opening brief 中的 Case 状态、当前搜索窗口(intake 不确定档)、做法三句要点与六类领域清单,并自然满足: - 三句模板:当前窗口与核对做法;「最后给区间和代表分钟,不给精确到秒」;「想到几件说几件,有大概年月就行」并点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤。 -- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。 +- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。只报一件也走既有逐领域采集;不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。 - 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。 - 不得写具体年份,不得要求先准备材料。 - 首题 `collect:other:*` 题干写成「先说你最容易想起的一两件,年月大概就行」。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.19/SKILL.md b/skills/jyotish-birth-time-rectification/versions/10.0.19/SKILL.md index 8455ba34..0a915db3 100644 --- a/skills/jyotish-birth-time-rectification/versions/10.0.19/SKILL.md +++ b/skills/jyotish-birth-time-rectification/versions/10.0.19/SKILL.md @@ -57,7 +57,7 @@ description: "生时校正专用 Skill(V10)。以服务器权威 Case、Conv 开场必须满足: -- 一条消息可以报多件;想到几件说几件,有大概年月即可。 +- 一条消息可以报多件;想到几件说几件,有大概年月即可。只报一件也继续既有逐领域采集,不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。 - 允许模糊日期:可以先说大概年份、阶段或范围;如确有信息增益,后续再澄清,不诱导猜测月份或日期。 - 首题保持采集题身份(`collect:other:*`),题干写成「先说你最容易想起的一两件,年月大概就行」。 - 至多一个主问题且唯一来源:每轮当前问题只能由服务端建立 `ConversationFocus` 并通过界面问题槽呈现。Agent 回复正文只做承接与解释,不得自行提出、复述、改写或预告问题;正文内容不参与问题槽判定。 diff --git a/skills/jyotish-birth-time-rectification/versions/10.0.19/references/conversation-strategy.md b/skills/jyotish-birth-time-rectification/versions/10.0.19/references/conversation-strategy.md index cd2b7f32..49de1898 100644 --- a/skills/jyotish-birth-time-rectification/versions/10.0.19/references/conversation-strategy.md +++ b/skills/jyotish-birth-time-rectification/versions/10.0.19/references/conversation-strategy.md @@ -18,7 +18,7 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什 首次开场只使用服务器 opening brief 中的 Case 状态、当前搜索窗口(intake 不确定档)、做法三句要点与六类领域清单,并自然满足: - 三句模板:当前窗口与核对做法;「最后给区间和代表分钟,不给精确到秒」;「想到几件说几件,有大概年月就行」并点出升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤。 -- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。 +- 一条消息可以报多件。不索要 10–15 条事件长表,不要一进场就出 A/B/C/D。只报一件也走既有逐领域采集;不得追问「还有吗 / 还能想起别的吗」,也不得重复开场邀请。第一道逐领域题干末尾由服务端带一次「想到别的也可以一起说」。 - 接受“大概某年 / 那几年 / 某个阶段”等模糊日期,不诱导猜月份、日期或精确时点。 - 不得写具体年份,不得要求先准备材料。 - 首题 `collect:other:*` 题干写成「先说你最容易想起的一两件,年月大概就行」。 diff --git a/skills/skill-package-registry.json b/skills/skill-package-registry.json index 5af60da5..2ad22e92 100644 --- a/skills/skill-package-registry.json +++ b/skills/skill-package-registry.json @@ -164,7 +164,7 @@ { "name": "jyotish-birth-time-rectification", "version": "10.0.19", - "sha256": "a21b33abbbe3a530d725b94719b57ff8417cfcb850c54b547d5c18e390ec3d2b", + "sha256": "a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8", "sourceCommit": null, "packagePath": "skills/jyotish-birth-time-rectification/versions/10.0.19", "status": "active"