fix(rectification): drop same-year probes after a dated answer (BUG-592)
Point-choice used one compare packet, so a May existence card and the same-domain year card both stayed eligible. Hard-exclude same domain+year; keep adjacent years downranked. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-08 — 答过某年某月后,不再问同一领域同一年的整年题
|
||||
|
||||
生时校正点选题里,已经答过「2023 年 5 月前后有没有…」之后,不会再问「2023 年前后有没有…」。相邻年仍可能问,因为分开的是另一组候选。Skill 版本仍是 10.0.15。
|
||||
|
||||
|
||||
## 2026-09-08 — 范围收到后不再把已答领域再问一遍
|
||||
|
||||
生时校正把七个带年月领域和职业问完、范围已经收到、决策已是采用时,下一步是区间交付卡。不会再把已经答过的财务或其他领域采集题拿出来问。同一道采集题撞过题号时,也不会改个后缀再问一次。Skill 版本仍是 10.0.15。
|
||||
|
||||
+19
-3
@@ -8647,9 +8647,9 @@
|
||||
|
||||
## BUG-559 | 同年月拆成多道「有没有 X」,同域同年再问,无锚点性格卡
|
||||
|
||||
- 状态:mitigated
|
||||
- 状态:mitigated(点选同年复发见 BUG-592)
|
||||
- 首次发现:2026-09-06
|
||||
- 最近更新:2026-09-06
|
||||
- 最近更新:2026-09-08
|
||||
- 影响面:`scripts/rectification/event_probes.py`、`asked_probe_keys`、`existenceProbeAsked`、`rankRenderableDiscriminators`
|
||||
- 用户现象:同一大运边界月按领域各问一次「那时候有没有入职 / 有没有搬家」;已问过某年 5 月后又问同年无月份;没有对应经历的「平时做事更接近哪一种」性格卡也会出。
|
||||
- 触发条件:引擎按领域扫描边界月;去重只看账本年份不看已问探针;`varga_style` 不要求同域带年月证据。
|
||||
@@ -8657,7 +8657,7 @@
|
||||
- 修复:请求可带 `asked_probe_keys`(不进结果指纹)。已问 `domain.Y.MM.*` 后,同年与相邻年不再出该领域 existence / activation。`varga_style` 无同域 confirmed 带年月事件则 `dropped_probes(unanchored_varga_style)`;有锚点时 hint 带年月、分数 ×0.8,用户可见题干仍不写年份。邻近账本事件以领域+年月(无摘要)写入 `user_prompt_hint`。B2 同年月合并卡 `choice_kind=boundary_pair` 本单未做。
|
||||
- 验证:`tests/test_rectification_event_probes.py` 已问同年相邻年不再产出;`frontend/tests/rectification-probe-year-dedupe-20260906.test.ts`。
|
||||
- 防复发:不得把 `asked_probe_keys` 写入结果指纹。点选答案仍不写证据账本。不得为性格卡在用户可见题干里写年份。
|
||||
- 相关记录:BUG-540、BUG-390
|
||||
- 相关记录:BUG-540、BUG-390、BUG-592
|
||||
- 复发自:无
|
||||
- 修复版本:`3a9ae736`
|
||||
|
||||
@@ -9174,3 +9174,19 @@
|
||||
- 复发自:BUG-462(`:next` 重试本为穷尽采集撞开场题号)
|
||||
- 修复版本:待提交(`codex/rectification-covered-domain-recollect-20260908`)
|
||||
|
||||
## BUG-592 | 答完「2023 年 5 月前后」又问「2023 年前后」(同域同年点选题)
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-08
|
||||
- 最近更新:2026-09-08
|
||||
- 影响面:`inspectDiscriminatorProbes`、`rankRenderableDiscriminators`、`existenceProbeAsked` / `sameYearProbeAsked`、点选后 `decideAfterInferenceChange`
|
||||
- 用户现象:已经答过某年某月「有没有入职/换工作」,下一张点选卡又问同一领域同一年的整年题,题干几乎同一句。
|
||||
- 触发条件:同一次候选比较生成的 `inference_state.probes` 里同时有 `domain.YYYY.MM.*` 和 `domain.YYYY.*`;点选不重跑引擎,按信息量从高到低问。
|
||||
- 根因:BUG-559 引擎侧只在重新生成时按 `asked_probe_keys` 屏蔽同年与相邻年;点选下一题走 TS 对照包。`inspectDiscriminatorProbes` 只认精确 `semantic_key` / hash / `probe_id`,没有领域+年份规则。`rankRenderableDiscriminators` 虽调 `existenceProbeAsked`,结果只是把 `asked=true` 交给 `rankDiscriminatorScore` 降权,不排除。旧验证只锁了引擎重生成和 `existenceProbeAsked` 返回值。
|
||||
- 修复:同领域同年份硬排除(`dropped_probes(reason="same_year_asked")`),`year=0` 的分盘风格题不动。相邻年(±1)继续降权、不硬排除。`existenceProbeAsked` 拆出 `sameYearProbeAsked`。引擎 `asked_probe_keys`、`SCORE_DELTA`、四选项合同、Skill 版本(仍 10.0.15)不动。
|
||||
- 验证:`frontend/tests/rectification-probe-year-dedupe-20260906.test.ts`:答完 5 月后不得再问同年 activation、相邻年 2024 仍可问、`buildMethodFollowupPlan` 同口径、事故五题回放第五题不再是同年整年题。`frontend/tests/rectification-inference-machine.test.ts` 锁核心 identity 不去重、inspect 硬排除。`tests/test_rectification_event_probes.py` 不改、须仍绿。
|
||||
- 防复发:同一份 probes 列表里,同领域同年份答过一道就不得再问第二道。不得只靠降权。相邻年不得被硬排除。点选路径必须走 `inspectDiscriminatorProbes` / `rankRenderableDiscriminators` 的同年硬排除,不能只依赖引擎重生成。
|
||||
- 相关记录:BUG-559
|
||||
- 复发自:BUG-559(引擎侧只管重生成;TS 侧只降权)
|
||||
- 修复版本:待提交(`codex/rectification-same-year-probe-dedupe-20260908`)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# PROGRESS · 同域同年点选题去重(2026-09-08)
|
||||
|
||||
工作树:`.worktrees/rectification-same-year-probe-dedupe-20260908`
|
||||
分支:`codex/rectification-same-year-probe-dedupe-20260908`
|
||||
任务书:`docs/tasks/TASK-rectification-same-year-probe-dedupe-20260908.md`
|
||||
基线:`origin/staging` @ `f17af254`(含 BUG-592 修复单 `f3a1a583`;代码基线仍是 `7ca6ade2`)
|
||||
|
||||
本单状态:**待验收**。
|
||||
|
||||
未改:引擎 `asked_probe_keys`、`SCORE_DELTA`、四选项合同、采用门、确认门、Skill 版本(仍 10.0.15)。相邻年仍只降权。
|
||||
|
||||
## 做了什么
|
||||
|
||||
- **决策 1** `sameYearProbeAsked`:同 `domain` 且同 `year`(`SEMANTIC_YEAR_KEY` 解析)硬排除。`inspectDiscriminatorProbes` 与 `rankRenderableDiscriminators`(event / contrast)都 `dropped_probes(reason="same_year_asked")`。`year=0` 分盘风格题不受影响。
|
||||
- **决策 2** 相邻年(±1)仍走 `existenceProbeAsked` → `rankDiscriminatorScore` 降权,不硬排除。`remainingReverseVerifyProbes` 继续按邻近年排除(采用后核对,不动)。
|
||||
- **决策 3** `remainingConflictProbes` 改用 `sameYearProbeAsked`,让邻近年事件探针还能进排序降权,而不是在进目录前被整年砍掉。
|
||||
- **决策 4** 记录 BUG-592(复发自 BUG-559)、CHANGELOG、手测脚本第 9 条。
|
||||
|
||||
## 三栏(被触碰断言)
|
||||
|
||||
| 用例 | 原值 | 新值 | 理由 |
|
||||
| --- | --- | --- | --- |
|
||||
| 答完 `career.2023.05.*` 后选 `career.2023.dasha_activation` | 会问(inspect 无年份规则;followup 只降权) | 不问,`dropped_probes.reason = same_year_asked` | 决策 1 |
|
||||
| 同列表 `career.2024.04.*` | 可问 | 仍可问 | 决策 2 |
|
||||
| 事故五题回放第五题 | `career.2023.dasha_activation` | D10 风格题或交付 | 决策 1 |
|
||||
| 核心 `isDuplicateProbe` | 不同 key 不算重复 | 仍不算;inspect 负责硬排除 | 旧防线缺口 |
|
||||
|
||||
## 测试
|
||||
|
||||
- `frontend` `./node_modules/.bin/tsc --noEmit`:exit 0
|
||||
- 任务书指定 TS 切片(`tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts`,排除 database):**1012 passed / 0 failed**
|
||||
- `.venv/bin/python -m pytest tests/test_rectification_event_probes.py -q`:34 passed
|
||||
- `npm run lint`:本机对改动文件 ReadLints 0 error;全量 `npm run lint` 未在本轮沙盒跑完
|
||||
- 浏览器真人走查:本轮未跑,见 `docs/testing/rectification-scenarios-20260907.md` §9
|
||||
- 工作期间 `origin/staging` 又合入 `06e44104`(BUG-593)。本分支仍基于 `f17af254`,未自动 merge(工作区有未提交文档与代码,且与 BUG-593 改了同一批 changelog / BUG_HISTORY / 手测脚本)
|
||||
|
||||
## 偏离
|
||||
|
||||
1. 任务书开工基线写 `7ca6ade2`。本 worktree 跟 `origin/staging` 尖端 `f17af254`(多了 BUG-593 修复单文档,无代码差)。
|
||||
2. `sameYearProbeAsked` 放在 `probe-question-contract.ts`,避免 `candidate-contrast-packet` ↔ `method-followup` 循环依赖。
|
||||
3. `remainingConflictProbes` 从邻近年硬排除改为仅同年硬排除,否则 event 探针路径仍会把有信息量的 2024 题提前砍掉。
|
||||
@@ -76,7 +76,7 @@
|
||||
| `TASK-rectification-other-collect-fallback-20260908.md` | `PROGRESS-rectification-other-collect-fallback-20260908.md` | 七领域问完落到「也可以再说一件」、职业题从未出现、不自动出卡:`USER_COLLECT_QUESTION.other` 仍有三条活路(职业焦点 `target_domain` 压成 other 后被 active-focus 承接 followup 重建;set-focus 两次无效兜底查表;无领域采集归 other),`collect:other:*` 焦点挂着让 `persistNextInterviewIfIdle` 的出卡路径永远不跑 | 已验收通过(触发链仍 investigating,BUG-586 mitigated) | `9aec5029`(BUG-586) |
|
||||
| `TASK-rectification-probe-replay-loss-20260908.md` | `PROGRESS-rectification-probe-replay-loss-20260908.md` | **P0** 收敛不了的直接原因:答完 5 道题范围收到 8 分钟,之后每补一件经历触发的重算换了候选集,`buildInferenceState` 找不到已答题定义(引擎与对照包按 asked keys 排除、状态 probes 不携带、分盘 key 内嵌分钟列表)→ `rounds:[]`、posterior=prior,交付回到 29 分钟;证据轮旁白不报范围变化;交付卡闪现待证实 | 已验收通过(真实 Case 范围收到 2 分钟;BUG-589 闪现 investigating) | `df182c16`(BUG-587~589) |
|
||||
| `TASK-rectification-covered-domain-recollect-20260908.md` | `PROGRESS-rectification-covered-domain-recollect-20260908.md` | 范围已收到 2 分钟、决策已是采用,却再问答过的财务题:职业覆盖后的 yearless→采集分支(L2224)不看领域是否已覆盖;`shouldSkipFollowupPersist` 把它当剩余采集挡住出卡;`persistCollectFocus` 撞 id 无条件 `:next` 再插;holdout `occupied` 不认 health/health_pressure 同义 | 已验收通过 | `7ca6ade2`(BUG-590~591) |
|
||||
| `TASK-rectification-same-year-probe-dedupe-20260908.md` | `PROGRESS-rectification-same-year-probe-dedupe-20260908.md` | 答完「2023 年 5 月前后」又问「2023 年前后」(BUG-559 复发):点选不重跑引擎,同一份 probes 里同域同年两题并存;TS 侧 `existenceProbeAsked` 只降权不排除,`inspectDiscriminatorProbes` 连领域+年份规则都没有。决策:同域同年硬排除、相邻年维持降权 | 待执行 | `codex/rectification-same-year-probe-dedupe-20260908`(BUG-592) |
|
||||
| `TASK-rectification-same-year-probe-dedupe-20260908.md` | `PROGRESS-rectification-same-year-probe-dedupe-20260908.md` | 答完「2023 年 5 月前后」又问「2023 年前后」(BUG-559 复发):点选不重跑引擎,同一份 probes 里同域同年两题并存;TS 侧 `existenceProbeAsked` 只降权不排除,`inspectDiscriminatorProbes` 连领域+年份规则都没有。决策:同域同年硬排除、相邻年维持降权 | 待验收 | `codex/rectification-same-year-probe-dedupe-20260908`(BUG-592) |
|
||||
| `TASK-rectification-delivery-report-facts-20260908.md` | `PROGRESS-rectification-delivery-report-facts-20260908.md` | 交付轮验证报告写「宽度 29 分钟」「主限偏向 05:06 / 分盘大运偏向 04:59」「04:53 D10 升狮子座」,与卡片(04:51–04:53)矛盾:`skill_verification_report` 用引擎推断前的 width / dasha_agreement,分盘星座让模型自算 | 待执行 | `codex/rectification-delivery-report-facts-20260908`(BUG-593,Skill 10.0.16) |
|
||||
| `TASK-api-not-configured-mislabel-20260904.md` | `PROGRESS-api-not-configured-mislabel-20260904.md` | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 已验收 | `5483649b`(BUG-542);2 条子进程测试留 CI Node 22 复核 |
|
||||
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) |
|
||||
|
||||
@@ -127,3 +127,15 @@
|
||||
- 若范围变了,证据轮旁白必须出现「范围从 … 变为 …」
|
||||
- 不得出现:只说「记下了」,交付时范围弹回大约半小时,候选分数跟没答过题一样
|
||||
|
||||
## 9. 同领域同年份的点选题只出一次
|
||||
|
||||
资料:家人记得大概时间,钟点任意,范围「差不多准」。地点任意公开城市。
|
||||
|
||||
开场后说到能出对照卡。若出现「某年某月前后有没有入职/换工作」,答「明确没有发生」。
|
||||
|
||||
期望:
|
||||
|
||||
- 下一张点选卡不得再问同一领域同一年的整年题(例如刚答过 2023 年 5 月,不得再问 2023 年前后)
|
||||
- 相邻年(例如 2024 年 4 月)仍可以问
|
||||
- 不得出现:同一句「有没有入职、换工作或职责加重」只把月份去掉再问一遍
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
isRenderableProbe,
|
||||
type DroppedProbe,
|
||||
rankDiscriminatorScore,
|
||||
sameYearProbeAsked,
|
||||
SEMANTIC_YEAR_KEY,
|
||||
} from "../v9/probe-question-contract.ts";
|
||||
|
||||
export type ContrastChoiceKind = "existence" | "varga_style" | "event_quality";
|
||||
@@ -405,6 +407,18 @@ export function vargaLayerFromSemanticKey(key: string): string | null {
|
||||
return key.match(/^varga\.(d\d+)/)?.[1] ?? null;
|
||||
}
|
||||
|
||||
function probeDomainYear(probe: Pick<CandidateDiscriminatorProbe, "domain" | "year" | "semanticKey">): {
|
||||
domain: string;
|
||||
year: number;
|
||||
} | null {
|
||||
if (probe.domain && probe.year && probe.year > 0) {
|
||||
return { domain: probe.domain, year: probe.year };
|
||||
}
|
||||
const match = probe.semanticKey.match(SEMANTIC_YEAR_KEY);
|
||||
if (!match) return null;
|
||||
return { domain: match[1], year: Number(match[2]) };
|
||||
}
|
||||
|
||||
export function inspectDiscriminatorProbes(
|
||||
packet: CandidateContrastPacket | null | undefined,
|
||||
options?: {
|
||||
@@ -461,6 +475,15 @@ export function inspectDiscriminatorProbes(
|
||||
});
|
||||
return [];
|
||||
}
|
||||
const domainYear = probeDomainYear(completed.probe);
|
||||
if (domainYear && sameYearProbeAsked(asked, domainYear.domain, domainYear.year)) {
|
||||
dropped.push({
|
||||
semantic_key: completed.probe.semanticKey,
|
||||
information_gain: completed.probe.informationGain,
|
||||
reason: "same_year_asked",
|
||||
});
|
||||
return [];
|
||||
}
|
||||
const layer = vargaLayerFromSemanticKey(completed.probe.semanticKey);
|
||||
const askedAlready = asked.has(completed.probe.semanticKey)
|
||||
|| asked.has(completed.probe.candidateSplitHash)
|
||||
|
||||
@@ -110,6 +110,8 @@ import {
|
||||
informationGainAmongActive,
|
||||
isRenderableProbe,
|
||||
rankDiscriminatorScore,
|
||||
sameYearProbeAsked,
|
||||
SEMANTIC_YEAR_KEY,
|
||||
EXISTENCE_STYLE_OPTIONS,
|
||||
type DroppedProbe,
|
||||
type ProbeStyleOption,
|
||||
@@ -142,6 +144,7 @@ import {
|
||||
} from "./window-widen.ts";
|
||||
import { DATE_RELIABILITY_PROMPT, pendingDateReliabilityEvidence } from "./date-reliability.ts";
|
||||
|
||||
export { sameYearProbeAsked };
|
||||
|
||||
export const METHOD_FOLLOWUP_IDS = [
|
||||
"dasha_events",
|
||||
@@ -335,18 +338,18 @@ function existenceNearbyYears(domain: string): number {
|
||||
return EXISTENCE_NEARBY_YEARS[domain] ?? 0;
|
||||
}
|
||||
|
||||
const SEMANTIC_YEAR = /^([a-z_]+)\.((?:19|20)\d{2})(?:\.|$)/;
|
||||
|
||||
export function existenceProbeAsked(
|
||||
askedKeys: ReadonlySet<string> | readonly string[],
|
||||
domain: string,
|
||||
year: number,
|
||||
): boolean {
|
||||
if (!domain || !year) return false;
|
||||
if (sameYearProbeAsked(askedKeys, domain, year)) return true;
|
||||
const asked = askedKeys instanceof Set ? askedKeys : new Set(askedKeys);
|
||||
const nearby = existenceNearbyYears(domain);
|
||||
if (nearby <= 0) return false;
|
||||
for (const key of asked) {
|
||||
const match = key.match(SEMANTIC_YEAR);
|
||||
const match = key.match(SEMANTIC_YEAR_KEY);
|
||||
if (!match || match[1] !== domain) continue;
|
||||
const askedYear = Number(match[2]);
|
||||
if (Number.isInteger(askedYear) && Math.abs(askedYear - year) <= nearby) return true;
|
||||
@@ -806,7 +809,7 @@ function remainingConflictProbes(
|
||||
if (
|
||||
askedKeys.has(semantic)
|
||||
|| (split && askedKeys.has(split))
|
||||
|| existenceProbeAsked(askedKeys, probe.domain, probe.year)
|
||||
|| sameYearProbeAsked(askedKeys, probe.domain, probe.year)
|
||||
) continue;
|
||||
rows.push(probe);
|
||||
}
|
||||
@@ -889,6 +892,9 @@ function renderableEventProbe(
|
||||
if (!rankedGain) {
|
||||
return { row: null, dropped: droppedFromProbe(key, probe.information_gain ?? 0, "no_split_among_active") };
|
||||
}
|
||||
if (sameYearProbeAsked(askedKeys, probe.domain, probe.year)) {
|
||||
return { row: null, dropped: droppedFromProbe(key, probe.information_gain ?? 0, "same_year_asked") };
|
||||
}
|
||||
const layer = vargaLayerFromSemanticKey(key);
|
||||
const asked = askedKeys.has(key)
|
||||
|| Boolean(probe.candidate_split_hash && askedKeys.has(probe.candidate_split_hash))
|
||||
@@ -960,10 +966,17 @@ function renderableContrastProbe(
|
||||
dropped: droppedFromProbe(working.semanticKey, working.informationGain, "no_split_among_active"),
|
||||
};
|
||||
}
|
||||
if (sameYearProbeAsked(askedKeys, working.domain, working.year)) {
|
||||
return {
|
||||
row: null,
|
||||
dropped: droppedFromProbe(working.semanticKey, working.informationGain, "same_year_asked"),
|
||||
};
|
||||
}
|
||||
const layer = vargaLayerFromSemanticKey(working.semanticKey);
|
||||
const asked = askedKeys.has(working.semanticKey)
|
||||
|| askedKeys.has(working.candidateSplitHash)
|
||||
|| askedKeys.has(working.probeId)
|
||||
|| existenceProbeAsked(askedKeys, working.domain ?? "", working.year ?? 0)
|
||||
|| (layer ? vargaLayerCovered(mentionedKeys, layer) : false);
|
||||
return {
|
||||
row: {
|
||||
|
||||
@@ -36,7 +36,8 @@ export type ProbeRejectReason =
|
||||
| "yearless_ungrounded_contrast"
|
||||
| "no_split_among_active"
|
||||
| "unanchored_varga_style"
|
||||
| "frameless_distinguish";
|
||||
| "frameless_distinguish"
|
||||
| "same_year_asked";
|
||||
|
||||
export type StyleOptionsResult =
|
||||
| { ok: true; options: ProbeStyleOption[] }
|
||||
@@ -312,6 +313,23 @@ export function discriminatorPriority(input: {
|
||||
return input.informationGain * novelty * coverage - penalty;
|
||||
}
|
||||
|
||||
export const SEMANTIC_YEAR_KEY = /^([a-z_]+)\.((?:19|20)\d{2})(?:\.|$)/;
|
||||
|
||||
export function sameYearProbeAsked(
|
||||
askedKeys: ReadonlySet<string> | readonly string[],
|
||||
domain: string | null | undefined,
|
||||
year: number | null | undefined,
|
||||
): boolean {
|
||||
if (!domain || !year) return false;
|
||||
const asked = askedKeys instanceof Set ? askedKeys : new Set(askedKeys);
|
||||
for (const key of asked) {
|
||||
const match = key.match(SEMANTIC_YEAR_KEY);
|
||||
if (!match || match[1] !== domain) continue;
|
||||
if (Number(match[2]) === year) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function rankDiscriminatorScore(input: {
|
||||
informationGain: number;
|
||||
asked?: boolean;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { clusterEquivalentCandidates } from "../src/lib/rectification-agentic/co
|
||||
import { evaluateConvergence } from "../src/lib/rectification-agentic/core/convergence-evaluator.ts";
|
||||
import { isDuplicateProbe } from "../src/lib/rectification-agentic/core/duplicate-probes.ts";
|
||||
import { selectHighestGainProbe } from "../src/lib/rectification-agentic/core/select-probe.ts";
|
||||
import { inspectDiscriminatorProbes } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
import { holdoutEventIds, splitHoldoutEvents } from "../src/lib/rectification-agentic/core/split-holdout.ts";
|
||||
import type { AnswerClass, ConflictProbe, InferenceCandidate, ProbeAnswer } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
|
||||
@@ -1079,3 +1080,78 @@ test("persisted focus can answer a lower-gain probe while conflicting schema ide
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("core identity does not treat same-year siblings as duplicates; inspect drops them", () => {
|
||||
const may = {
|
||||
...probe({
|
||||
id: "probe:career.2023.05.dasha_boundary",
|
||||
domain: "career",
|
||||
year: 2023,
|
||||
gain: 1.1,
|
||||
yesSupports: ["05:00"],
|
||||
yesConflicts: ["05:20"],
|
||||
split: "career.2023.05.dasha_boundary",
|
||||
}),
|
||||
semantic_key: "career.2023.05.dasha_boundary",
|
||||
};
|
||||
const activation = {
|
||||
...probe({
|
||||
id: "probe:career.2023.dasha_activation",
|
||||
domain: "career",
|
||||
year: 2023,
|
||||
gain: 0.46,
|
||||
yesSupports: ["05:00"],
|
||||
yesConflicts: ["05:20"],
|
||||
split: "career.2023.dasha_activation",
|
||||
}),
|
||||
semantic_key: "career.2023.dasha_activation",
|
||||
};
|
||||
const nearby = {
|
||||
...probe({
|
||||
id: "probe:career.2024.04.dasha_boundary",
|
||||
domain: "career",
|
||||
year: 2024,
|
||||
gain: 0.97,
|
||||
yesSupports: ["05:00"],
|
||||
yesConflicts: ["05:20"],
|
||||
split: "career.2024.04.dasha_boundary",
|
||||
}),
|
||||
semantic_key: "career.2024.04.dasha_boundary",
|
||||
};
|
||||
const asked = [{
|
||||
probe_id: may.id,
|
||||
semantic_key: may.semantic_key,
|
||||
candidate_split_hash: may.candidate_split_hash,
|
||||
answer_class: "no" as const,
|
||||
classified_from: "choice" as const,
|
||||
}];
|
||||
assert.equal(isDuplicateProbe(activation, asked), false);
|
||||
assert.equal(selectHighestGainProbe([activation, nearby], asked)?.id, nearby.id);
|
||||
const packet = {
|
||||
candidateSetVersion: "05:00-05:20",
|
||||
vargaDifferences: [] as const,
|
||||
probes: [activation, nearby].map((item) => ({
|
||||
probeId: item.id,
|
||||
candidateSetVersion: "05:00-05:20",
|
||||
question: item.question,
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "yes", supportsCandidateIds: [...item.expected_outcomes[0].supports], conflictsCandidateIds: [...item.expected_outcomes[0].conflicts] },
|
||||
{ outcomeId: "no", supportsCandidateIds: [...item.expected_outcomes[1].supports], conflictsCandidateIds: [...item.expected_outcomes[1].conflicts] },
|
||||
],
|
||||
candidateSplitHash: item.candidate_split_hash,
|
||||
informationGain: item.information_gain,
|
||||
sourceFeatures: [{ technique: "Vimshottari", calculationResultId: null }],
|
||||
domain: item.domain,
|
||||
year: item.year,
|
||||
semanticKey: item.semantic_key,
|
||||
choiceKind: "existence" as const,
|
||||
})),
|
||||
};
|
||||
const inspected = inspectDiscriminatorProbes(packet, { askedKeys: asked.map((item) => item.semantic_key) });
|
||||
assert.notEqual(inspected.selected?.semanticKey, activation.semantic_key);
|
||||
assert.equal(inspected.selected?.semanticKey, nearby.semantic_key);
|
||||
assert.equal(
|
||||
inspected.dropped.some((item) => item.semantic_key === activation.semantic_key && item.reason === "same_year_asked"),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts";
|
||||
import { inspectDiscriminatorProbes } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
import { INFERENCE_ALGORITHM_VERSION } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
import { decideAfterInferenceChange } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import type { DecisionDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import {
|
||||
buildMethodFollowupPlan,
|
||||
datedLedgerAnchor,
|
||||
existenceProbeAsked,
|
||||
remainingReverseVerifyProbes,
|
||||
sameYearProbeAsked,
|
||||
type MethodFollowupEvidence,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import type { CandidateContrastPacket } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
import { askedSemanticKeysForEngine } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import { engineRequestBody, toEngineEvents } from "../src/lib/rectification-agentic/v9/engine-client.ts";
|
||||
import { EXISTENCE_STYLE_OPTIONS } from "../src/lib/rectification-agentic/v9/probe-question-contract.ts";
|
||||
|
||||
function existenceProbe(
|
||||
domain: DiscriminatingEventProbe["domain"],
|
||||
year: number,
|
||||
extra: { month?: number; source?: DiscriminatingEventProbe["source"]; key?: string } = {},
|
||||
extra: { month?: number; source?: DiscriminatingEventProbe["source"]; key?: string; gain?: number } = {},
|
||||
): DiscriminatingEventProbe {
|
||||
const month = extra.month;
|
||||
const source = extra.source ?? "dasha_boundary";
|
||||
@@ -29,14 +38,14 @@ function existenceProbe(
|
||||
year_label: month ? `${year} 年 ${month} 月前后` : `${year} 年前后`,
|
||||
month,
|
||||
domain,
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
event_family: domain === "relocation" ? "搬家或长期住到外地" : "入职、升职或职责明显加重",
|
||||
source,
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: `时间范围锁定 ${year} 年。`,
|
||||
role: "distinguish",
|
||||
information_gain: 1.1,
|
||||
information_gain: extra.gain ?? 1.1,
|
||||
semantic_key: key,
|
||||
candidate_split_hash: key,
|
||||
candidate_ids: ["05:00", "05:20"],
|
||||
@@ -201,3 +210,340 @@ test("after a D9-style answer the compare request body stays legal", () => {
|
||||
const keys = (body.asked_probe_keys as string[] | undefined) ?? [];
|
||||
assert.ok(keys.every((key) => key.length <= 120 && !key.includes(":varga.")));
|
||||
});
|
||||
|
||||
const COVERED_FOR_DISCRIMINATE: MethodFollowupEvidence[] = [
|
||||
dated("e-edu", "education", "education_start", "2016-09-01"),
|
||||
dated("e-rel", "relationship", "relationship_start", "2018-05-01"),
|
||||
dated("e-career", "career", "career_entry", "2020-04-01"),
|
||||
dated("e-fam", "family", "family_event", "2019-03-01"),
|
||||
{
|
||||
id: "e-occ",
|
||||
status: "confirmed",
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown",
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
},
|
||||
];
|
||||
|
||||
const CAREER_2023_05_KEY = "career.2023.05.dasha_boundary";
|
||||
const CAREER_2023_ACTIVATION_KEY = "career.2023.dasha_activation";
|
||||
const CAREER_2024_04_KEY = "career.2024.04.dasha_boundary";
|
||||
const CAREER_2023_05 = existenceProbe("career", 2023, { month: 5, gain: 1.1, key: CAREER_2023_05_KEY });
|
||||
const CAREER_2023_ACTIVATION = existenceProbe("career", 2023, {
|
||||
source: "dasha_activation",
|
||||
gain: 0.46,
|
||||
key: CAREER_2023_ACTIVATION_KEY,
|
||||
});
|
||||
const CAREER_2024_04 = existenceProbe("career", 2024, { month: 4, gain: 0.97, key: CAREER_2024_04_KEY });
|
||||
const RELOCATION_2015_05 = existenceProbe("relocation", 2015, { month: 5, gain: 0.74 });
|
||||
|
||||
const D9_STYLE: CandidateContrastPacket["probes"][number] = {
|
||||
...D10_STYLE,
|
||||
probeId: "contrast:varga.d9.巨蟹座/狮子座",
|
||||
question: "亲密关系里更接近下面哪一种相处方式?",
|
||||
candidateSplitHash: "varga.d9.巨蟹座/狮子座",
|
||||
informationGain: 1.53,
|
||||
sourceFeatures: [{ technique: "D9", calculationResultId: null }],
|
||||
domain: "relationship",
|
||||
semanticKey: "varga.d9.巨蟹座/狮子座",
|
||||
styleOptions: [
|
||||
{ label: "相处里更主动,也更愿意把关系往前推", answerClass: "yes", sign: "巨蟹座" },
|
||||
{ label: "相处里更克制,先把分寸看清楚", answerClass: "weak_yes", sign: "狮子座" },
|
||||
],
|
||||
};
|
||||
|
||||
const D10_LOW: CandidateContrastPacket["probes"][number] = {
|
||||
...D10_STYLE,
|
||||
informationGain: 0.4,
|
||||
};
|
||||
|
||||
function contrastFromEvent(probe: DiscriminatingEventProbe): CandidateContrastPacket["probes"][number] {
|
||||
const yes = probe.expected_outcomes?.find((row) => row.answer_class === "yes")?.supports ?? ["05:00"];
|
||||
const no = probe.expected_outcomes?.find((row) => row.answer_class === "no")?.supports ?? ["05:20"];
|
||||
const key = probe.semantic_key ?? `${probe.domain}.${probe.year}`;
|
||||
return {
|
||||
probeId: `probe:${key}`,
|
||||
candidateSetVersion: "05:00-05:20",
|
||||
question: probe.year_label,
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "yes", supportsCandidateIds: [...yes], conflictsCandidateIds: [...no] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
{ outcomeId: "no", supportsCandidateIds: [...no], conflictsCandidateIds: [...yes] },
|
||||
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
],
|
||||
candidateSplitHash: probe.candidate_split_hash ?? key,
|
||||
informationGain: probe.information_gain ?? 0,
|
||||
sourceFeatures: [{ technique: "Vimshottari", calculationResultId: null }],
|
||||
domain: probe.domain,
|
||||
year: probe.year,
|
||||
semanticKey: key,
|
||||
choiceKind: "existence",
|
||||
};
|
||||
}
|
||||
|
||||
function accidentPacket(probes: readonly CandidateContrastPacket["probes"][number][]): CandidateContrastPacket {
|
||||
return {
|
||||
candidateSetVersion: "05:00-05:20",
|
||||
vargaDifferences: [],
|
||||
probes,
|
||||
};
|
||||
}
|
||||
|
||||
function conflictFromContrast(probe: CandidateContrastPacket["probes"][number]): ConflictProbe {
|
||||
const yes = probe.expectedOutcomes.find((row) => row.outcomeId === "yes")?.supportsCandidateIds ?? ["05:00"];
|
||||
const no = probe.expectedOutcomes.find((row) => row.outcomeId === "no")?.supportsCandidateIds ?? ["05:20"];
|
||||
return {
|
||||
id: probe.probeId,
|
||||
semantic_key: probe.semanticKey,
|
||||
candidate_split_hash: probe.candidateSplitHash,
|
||||
domain: probe.domain ?? "career",
|
||||
year: probe.year && probe.year > 0 ? probe.year : 0,
|
||||
question: probe.question,
|
||||
candidate_ids: [...new Set([...yes, ...no])],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: [...yes], conflicts: [...no] },
|
||||
{ answer_class: "weak_yes", supports: [], conflicts: [] },
|
||||
{ answer_class: "no", supports: [...no], conflicts: [...yes] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: probe.informationGain,
|
||||
source: probe.choiceKind === "varga_style" ? "varga_contrast" : "dasha_boundary",
|
||||
choice_kind: probe.choiceKind ?? "existence",
|
||||
style_options: probe.styleOptions
|
||||
? probe.styleOptions.map((item) => ({
|
||||
label: item.label,
|
||||
answer_class: item.answerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
}))
|
||||
: [...EXISTENCE_STYLE_OPTIONS],
|
||||
};
|
||||
}
|
||||
|
||||
function answered(probe: { probeId?: string; id?: string; semanticKey?: string; semantic_key?: string; candidateSplitHash?: string; candidate_split_hash?: string }) {
|
||||
return {
|
||||
probe_id: probe.probeId ?? probe.id ?? "",
|
||||
semantic_key: probe.semanticKey ?? probe.semantic_key ?? "",
|
||||
candidate_split_hash: probe.candidateSplitHash ?? probe.candidate_split_hash ?? "",
|
||||
answer_class: "no" as const,
|
||||
classified_from: "choice" as const,
|
||||
};
|
||||
}
|
||||
|
||||
function accidentState(
|
||||
remaining: readonly CandidateContrastPacket["probes"][number][],
|
||||
asked: readonly ReturnType<typeof answered>[],
|
||||
) {
|
||||
const times = ["05:00", "05:20"] as const;
|
||||
const probes = remaining.map(conflictFromContrast);
|
||||
const raw = {
|
||||
algorithm_version: INFERENCE_ALGORITHM_VERSION,
|
||||
candidate_set_id: candidateSetId("05:00", "05:20", times),
|
||||
revision: asked.length + 1,
|
||||
phase: "discrimination" as const,
|
||||
result_status: "discriminating" as const,
|
||||
range_start: "05:00",
|
||||
range_end: "05:20",
|
||||
candidates: times.map((time, index) => ({
|
||||
id: time,
|
||||
time,
|
||||
cluster_range: [time, time] as const,
|
||||
prior_score: time === "05:00" ? 20 : 10,
|
||||
posterior_score: time === "05:00" ? 20 : 10,
|
||||
probability: time === "05:00" ? 0.67 : 0.33,
|
||||
status: "active" as const,
|
||||
rank: index + 1,
|
||||
strong_conflict_count: 0,
|
||||
})),
|
||||
events: [
|
||||
{ id: "e-edu", domain: "education", year: 2016, precision: "month" as const, usage: "training" as const },
|
||||
{ id: "e-rel", domain: "relationship", year: 2018, precision: "month" as const, usage: "training" as const },
|
||||
{ id: "e-career", domain: "career", year: 2020, precision: "month" as const, usage: "training" as const },
|
||||
{ id: "e-fam", domain: "family", year: 2019, precision: "month" as const, usage: "holdout" as const },
|
||||
],
|
||||
probes: [
|
||||
...asked.map((item) => remaining.find((probe) => probe.semanticKey === item.semantic_key)).filter(Boolean).map((probe) => conflictFromContrast(probe as CandidateContrastPacket["probes"][number])),
|
||||
...probes,
|
||||
],
|
||||
answered_probes: asked,
|
||||
rounds: [],
|
||||
last_inference_round: null,
|
||||
entropy: 1.2,
|
||||
representative_time: "05:00",
|
||||
credible_range: ["05:00", "05:20"] as const,
|
||||
holdout_passed: null,
|
||||
};
|
||||
const loaded = asInferenceState(raw);
|
||||
assert.ok(loaded, "accident inference fixture must pass asInferenceState");
|
||||
return loaded;
|
||||
}
|
||||
|
||||
function accidentDossier(
|
||||
remaining: readonly CandidateContrastPacket["probes"][number][],
|
||||
asked: readonly ReturnType<typeof answered>[],
|
||||
): DecisionDossier {
|
||||
const state = accidentState(remaining, asked);
|
||||
return {
|
||||
evidence: COVERED_FOR_DISCRIMINATE,
|
||||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||||
latestResult: {
|
||||
resultId: "55555555-5555-4555-8555-555555555555",
|
||||
selectionAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
evidenceLedgerFingerprint: "fp-same-year",
|
||||
candidates: [
|
||||
{ candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", time: "05:00", rank: 1, relativeSupport: 20 },
|
||||
{ candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2", time: "05:20", rank: 2, relativeSupport: 10 },
|
||||
],
|
||||
representativeTime: "05:00",
|
||||
decisionReceipt: {
|
||||
accept_allowed: true,
|
||||
acceptance_allowed: true,
|
||||
propose_allowed: true,
|
||||
selection_allowed: true,
|
||||
confirmation_allowed: false,
|
||||
inference_state: state,
|
||||
},
|
||||
},
|
||||
case: { acceptedTime: null, status: "active" },
|
||||
};
|
||||
}
|
||||
|
||||
test("sameYearProbeAsked is exact domain+year; nearby years stay on existenceProbeAsked", () => {
|
||||
const asked = ["career.2023.05.dasha_boundary"];
|
||||
assert.equal(sameYearProbeAsked(asked, "career", 2023), true);
|
||||
assert.equal(sameYearProbeAsked(asked, "career", 2024), false);
|
||||
assert.equal(sameYearProbeAsked(asked, "relocation", 2023), false);
|
||||
assert.equal(sameYearProbeAsked(asked, "career", 0), false);
|
||||
assert.equal(existenceProbeAsked(asked, "career", 2024), true);
|
||||
});
|
||||
|
||||
test("after a May 2023 career probe, the same-year activation card is dropped not ranked", () => {
|
||||
const packet = accidentPacket([
|
||||
contrastFromEvent(CAREER_2023_05),
|
||||
contrastFromEvent(CAREER_2023_ACTIVATION),
|
||||
]);
|
||||
const inspected = inspectDiscriminatorProbes(packet, {
|
||||
askedKeys: [CAREER_2023_05_KEY],
|
||||
});
|
||||
assert.notEqual(inspected.selected?.semanticKey, CAREER_2023_ACTIVATION.semantic_key);
|
||||
assert.equal(
|
||||
inspected.dropped.some((item) => (
|
||||
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
|
||||
)),
|
||||
true,
|
||||
);
|
||||
|
||||
const decided = decideAfterInferenceChange({
|
||||
dossier: accidentDossier(
|
||||
[contrastFromEvent(CAREER_2023_05), contrastFromEvent(CAREER_2023_ACTIVATION)],
|
||||
[answered(contrastFromEvent(CAREER_2023_05))],
|
||||
),
|
||||
state: accidentState(
|
||||
[contrastFromEvent(CAREER_2023_ACTIVATION)],
|
||||
[answered(contrastFromEvent(CAREER_2023_05))],
|
||||
),
|
||||
userStopped: false,
|
||||
birthDate: "1997-08-08",
|
||||
});
|
||||
assert.notEqual(decided.probe?.semanticKey, CAREER_2023_ACTIVATION.semantic_key);
|
||||
assert.equal(
|
||||
decided.droppedProbes.some((item) => (
|
||||
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
|
||||
)),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("adjacent-year career probes stay eligible after a 2023 answer", () => {
|
||||
const packet = accidentPacket([
|
||||
contrastFromEvent(CAREER_2023_ACTIVATION),
|
||||
contrastFromEvent(CAREER_2024_04),
|
||||
]);
|
||||
const inspected = inspectDiscriminatorProbes(packet, {
|
||||
askedKeys: [CAREER_2023_05_KEY],
|
||||
});
|
||||
assert.equal(inspected.selected?.semanticKey, CAREER_2024_04.semantic_key);
|
||||
assert.equal(
|
||||
inspected.dropped.some((item) => (
|
||||
item.semantic_key === CAREER_2024_04.semantic_key && item.reason === "same_year_asked"
|
||||
)),
|
||||
false,
|
||||
);
|
||||
|
||||
const decided = decideAfterInferenceChange({
|
||||
dossier: accidentDossier(
|
||||
[contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04)],
|
||||
[answered(contrastFromEvent(CAREER_2023_05))],
|
||||
),
|
||||
state: accidentState(
|
||||
[contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04)],
|
||||
[answered(contrastFromEvent(CAREER_2023_05))],
|
||||
),
|
||||
userStopped: false,
|
||||
birthDate: "1997-08-08",
|
||||
});
|
||||
assert.equal(decided.probe?.semanticKey, CAREER_2024_04.semantic_key);
|
||||
});
|
||||
|
||||
test("buildMethodFollowupPlan drops the same-year activation after the May card", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: COVERED_FOR_DISCRIMINATE,
|
||||
contrastPacket: accidentPacket([
|
||||
contrastFromEvent(CAREER_2023_05),
|
||||
contrastFromEvent(CAREER_2023_ACTIVATION),
|
||||
contrastFromEvent(CAREER_2024_04),
|
||||
]),
|
||||
askedProbeKeys: [CAREER_2023_05_KEY],
|
||||
candidatesSeparated: false,
|
||||
birthDate: "1997-08-08",
|
||||
});
|
||||
assert.notEqual(plan.next_followup?.semantic_key, CAREER_2023_ACTIVATION.semantic_key);
|
||||
assert.equal(
|
||||
plan.dropped_probes.some((item) => (
|
||||
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
|
||||
)),
|
||||
true,
|
||||
);
|
||||
assert.equal(plan.next_followup?.semantic_key, CAREER_2024_04.semantic_key);
|
||||
});
|
||||
|
||||
test("accident replay: fifth card is not the 2023 activation after four answers", () => {
|
||||
const pool = [
|
||||
D9_STYLE,
|
||||
contrastFromEvent(CAREER_2023_05),
|
||||
contrastFromEvent(CAREER_2024_04),
|
||||
contrastFromEvent(RELOCATION_2015_05),
|
||||
contrastFromEvent(CAREER_2023_ACTIVATION),
|
||||
D10_LOW,
|
||||
];
|
||||
const asked = [
|
||||
answered(D9_STYLE),
|
||||
answered(contrastFromEvent(CAREER_2023_05)),
|
||||
answered(contrastFromEvent(CAREER_2024_04)),
|
||||
answered(contrastFromEvent(RELOCATION_2015_05)),
|
||||
];
|
||||
const remaining = [
|
||||
contrastFromEvent(CAREER_2023_ACTIVATION),
|
||||
D10_LOW,
|
||||
];
|
||||
const decided = decideAfterInferenceChange({
|
||||
dossier: accidentDossier(pool, asked),
|
||||
state: accidentState(remaining, asked),
|
||||
userStopped: false,
|
||||
birthDate: "1997-08-08",
|
||||
});
|
||||
assert.notEqual(decided.probe?.semanticKey, CAREER_2023_ACTIVATION.semantic_key);
|
||||
assert.equal(
|
||||
decided.droppedProbes.some((item) => (
|
||||
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
|
||||
)),
|
||||
true,
|
||||
);
|
||||
assert.ok(
|
||||
decided.probe?.semanticKey === D10_LOW.semanticKey
|
||||
|| decided.nextAction === "ready_to_adopt"
|
||||
|| decided.sessionOutcome === "adopt_representative",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user