fix(rectification): centralize delivery authority
Independent Staging Quality Gate / validate (push) Successful in 13m0s
Independent Staging Quality Gate / publish (push) Successful in 10m58s

This commit is contained in:
Jesse_Chen
2026-09-01 01:13:04 +08:00
parent 9aebd0474a
commit 9f011194f2
23 changed files with 764 additions and 170 deletions
@@ -0,0 +1,173 @@
# 生时校正决策权威重构进度(2026-08-31)
## 基线与交付边界
- 隔离 worktree:`/Users/jesse/Downloads/Copse/astrology/.worktrees/rectification-decision-authority-20260831`
- 分支:`codex/rectification-decision-authority-20260831`
- 基线:`origin/staging` @ `9aebd0474a089ed5bec1fc8e8432d0d85e93ffb6`
- 2026-08-31 交付前重新执行 `git fetch origin staging --prune`;本地基线与远端 `staging` 仍一致。
- 本次只交付任务 0、A、B。任务 C、D 尚未开始;不部署,不提升 `main`,不声称真实 authenticated staging flow 已验证。
## 任务 0:不变量门控
实现前先加入五条不变量并运行,五条均红,且失败原因分别落在任务书列出的病灶:
1. TypeScript 能力超过引擎 ceiling。
2. 证据不足仍允许采用/选择。
3. 候选并列或未分离仍允许采用。
4. holdout unavailable 仍允许采用。
5. public overlay 用本地字段覆盖引擎否决。
A+B 完成后五条全部转绿:`tests 5 / pass 5 / fail 0`。
## 任务 A:停止原因分类化
- 新增三类 `StopClass`:`keep_collecting`、`exhausted`、`user_stopped`。
- `insufficient_dated_events` 与 `insufficient_domains` 只进入收集流程,不再进入 `completeWithRange()`。
- 真正耗尽(并列第一、用户不确定度过高)仍可结束为 review-only range。
- 用户主动停止改变的是“是否继续追问”,不是证据是否达标:允许交付区间,但不放开采用能力。
- 空候选不再返回无停止原因的 fail-open 状态,而是归入证据不足、继续收集。
- `decideNextAction()` 与 `decideRectification()` 同步演进,保留既有一致性断言。
## 任务 B:能力标志单一来源与引擎上限
- 新增严格四字段 `EngineCapabilityCeiling`:
- `acceptanceAllowed`
- `selectionAllowed`
- `proposeAllowed`
- `confirmationAllowed`
- `decisionReceipt` 四字段缺失、非 boolean 或字段关系非法时 fail-closed。raw dossier receipt 还会严格校验 `accept_allowed` / `confirm_allowed` alias 与 canonical 字段一致,并校验 `display_allowed` 不得与已允许 selection 的结果冲突。
- `deliveryCapability()` 成为以下四字段的唯一计算点:
- `canAdopt`
- `selectionAllowed`
- `proposeAllowed`
- `canConfirmExactMinute`
- 本地证据、coverage、holdout、separation 门与引擎 ceiling 取交集;TypeScript 只能收紧,不能放宽。
- `overlayPublicDecision()` 对四个门禁字段取本地结果与 receipt ceiling 的逻辑与,保留候选投影不一致时的 fail-closed。
- `v9/engine-client.ts` 扩展返回四个引擎能力字段。
- `v9/decision-from-dossier.ts` 三个调用点、`core/decide-next-action.ts`、`v9/method-followup.ts` 已逐一处理。
- `method-followup.ts` 的 helper 不参与生产交付,只用于问题流程,因此显式传关闭 ceiling;流程走向仍可 collect/discriminate,不会因四个交付能力全 false 而静默。
- `v9/answer-choice.ts` 的采用 narration 改为绑定最终能力 `can_offer_range && can_adopt`,避免 `ready_to_adopt` 丢失同回合采用载体。
- `v9/turn-decision.ts` 继续写入 `decision.selectionAllowed`;该字段现在已是 `deliveryCapability()` 的最终结果,无另行判门。
- `frontend/src/lib/rectification-agentic/rectification-agentic/session.ts` 在 `frontend/src` 下无生产 import,属于非活跃 legacy 路径,本轮不改。
- `decideConversationalSession()` 仍未接入生产路径;既有“生产工具不得调用它”的测试约束未解除。
## B-1 grep 自证
以下六个分支函数内,四个能力字段均无 boolean 字面量赋值:
```text
collect: OK
discriminate: OK
offerRangeWithoutAdopt: OK
holdoutValidation: OK
completeWithRange: OK
finish: OK
```
## 事故输入回归
输入:
- 1 条日期证据
- 1 个 domain
- 9 个候选分数:`12,12,12,12,11,11,10,10,10`
- `trainingGateOpen=false`
- `methodCoverageAll=false`
- `snapshotCurrent=true`
- 引擎 ceiling 四字段全 false
重构前任务书记录:
```json
{
"canAdopt": true,
"selectionAllowed": true,
"nextAction": "complete_with_range",
"stopReason": "insufficient_dated_events"
}
```
当前输出:
```json
{
"canAdopt": false,
"selectionAllowed": false,
"proposeAllowed": false,
"canConfirmExactMinute": false,
"nextAction": "ask_fact_collection",
"sessionOutcome": "collect_evidence",
"stopReason": "insufficient_dated_events",
"canOfferRange": false
}
```
## 既有断言修改配额
按用户批准的新口径:
- 错误行为组:`6 / 6`
- 修改的既有断言:`10 / 12`
前三条先前未列明的断言为:
1. `frontend/tests/rectification-convergence-budget.test.ts:172`:`canAdopt true → false`;病灶 3,候选未分开且预算耗尽只能交付 review-only range。
2. `frontend/tests/rectification-convergence-budget.test.ts:412`:label `blocked → user_history_verification_required`;病灶 1,空候选必须分类为证据不足而不是无原因 fail-open。
3. `frontend/tests/rectification-decide-next-action.test.ts:511`:`sessionOutcome adopt_representative → provisional_range`;病灶 4,holdout unavailable 不能进入采用态。
完整清单(按错误行为组):
### 组 1:候选未分离却进入采用态(病灶 3)
1. `frontend/tests/rectification-answer-choice.test.ts:608`:`nextAction offer_provisional_range → ready_to_adopt`。原 fixture 为 `16/14`,回答后仍未达到 separation 门却依赖错误采用行为;fixture 已收窄修正为刚好达门的 `18/14` 并通过 holdout,正确采用态是 `ready_to_adopt`。
2. `frontend/tests/rectification-convergence-budget.test.ts:172`:`canAdopt true → false`。预算耗尽不等于候选已分开。
### 组 2:空候选缺少停止分类(病灶 1)
3. `frontend/tests/rectification-convergence-budget.test.ts:412`:label `blocked → user_history_verification_required`。空候选应继续收集日期证据。
### 组 3:无 holdout 的可信区间被当作可采用(病灶 4)
4. `frontend/tests/rectification-credible-range-projection.test.ts:146`:`nextAction ready_to_adopt → offer_provisional_range`。无 holdout 只能 review-only。
### 组 4:单候选但 holdout unavailable 被当作可采用(病灶 4)
5. `frontend/tests/rectification-decide-next-action.test.ts:511`:`sessionOutcome adopt_representative → provisional_range`。单候选不覆盖独立核对门。
### 组 5:sticky holdout 失去日期精度后仍可采用(病灶 4)
6. `frontend/tests/rectification-holdout-renderable.test.ts:151`:`nextAction ready_to_adopt → offer_provisional_range`。
7. `frontend/tests/rectification-holdout-renderable.test.ts:153`:`sessionOutcome adopt_representative → provisional_range`。
8. `frontend/tests/rectification-holdout-renderable.test.ts:157`:`canAdopt true → false`。
三条均由同一错误行为产生:sticky 只保留 holdout 身份,unknown precision 使其不可验证,必须 fail-closed。
### 组 6:用户停止绕过证据与 coverage 门(病灶 2)
9. `frontend/tests/rectification-range-offer-deadend.test.ts:601`:`canAdopt true → false`。用户停止只允许展示区间。
10. `frontend/tests/rectification-range-offer-deadend.test.ts:608`:`canAdopt true → false`。两条同域日期事件仍不满足独立交付采用门。
`frontend/tests/rectification-holdout-renderable.test.ts` 中 `passed holdout` 的 `canAdopt === true` 保持不变;只补全 receipt fixture。
## 未放宽的门槛
- `MIN_STANDALONE_DATED_EVENTS = 3`
- `MIN_STANDALONE_DATED_DOMAINS = 2`
- `MIN_SEPARATION_LEAD = 8`
- 4 事件 / 3 domain 的 exact-minute confirmation gate
## 本地验收
- `./node_modules/.bin/tsx --test --test-name-pattern='^invariant [1-5]:' tests/rectification-decision-authority.test.ts`:`5/5` 通过。
- `./node_modules/.bin/tsx --test tests/rectification-answer-choice.test.ts`:`27/27` 通过。
- `./node_modules/.bin/tsx --test tests/rectification-*.test.ts`:`712/712` 通过,`fail=0`。
- `./node_modules/.bin/tsc --noEmit`:exit 0。
- `git diff --check`:exit 0。
- `.gitea/workflows/**`:无改动。
## 尚未执行
- 任务 C:前端 selection card 的独立 receipt fail-closed。
- 任务 D:问题槽单一文案源与 Skill `10.0.14` 五处同步升版。
- staging 部署与真实 authenticated flow 验证。
@@ -56,8 +56,12 @@ export function decideNextAction(input: DecideNextActionInput): RectificationNex
holdoutValidation: input.holdoutValidation,
accepted: input.accepted,
inferenceCredibleRange: input.inferenceCredibleRange,
engineAcceptAllowed: input.selectionAllowed,
engineProposeAllowed: input.proposeAllowed,
engineCeiling: {
acceptanceAllowed: input.selectionAllowed === true,
selectionAllowed: input.selectionAllowed === true,
proposeAllowed: input.proposeAllowed === true,
confirmationAllowed: input.confirmationAllowed === true,
},
inferenceRounds: input.inferenceRounds,
effectiveAnswerCount: input.effectiveAnswerCount,
plateauRounds: input.plateauRounds,
@@ -38,6 +38,61 @@ export type EvidenceStopReason =
| "tied_first"
| "user_uncertainty_too_high";
export type StopClass =
| Readonly<{ kind: "keep_collecting"; reason: "insufficient_dated_events" | "insufficient_domains" }>
| Readonly<{ kind: "exhausted"; reason: "tied_first" | "user_uncertainty_too_high" }>
| Readonly<{ kind: "user_stopped" }>;
export type EngineCapabilityCeiling = Readonly<{
acceptanceAllowed: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
confirmationAllowed: boolean;
}>;
type DeliveryCapability = Readonly<{
canAdopt: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
canConfirmExactMinute: boolean;
}>;
const CLOSED_ENGINE_CAPABILITY_CEILING: EngineCapabilityCeiling = {
acceptanceAllowed: false,
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: false,
};
export function engineCapabilityCeilingFromReceipt(value: unknown): EngineCapabilityCeiling {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return CLOSED_ENGINE_CAPABILITY_CEILING;
}
const row = value as Readonly<Record<string, unknown>>;
const acceptanceAllowed = row.acceptance_allowed;
const selectionAllowed = row.selection_allowed;
const proposeAllowed = row.propose_allowed;
const confirmationAllowed = row.confirmation_allowed;
if (
typeof acceptanceAllowed !== "boolean"
|| typeof selectionAllowed !== "boolean"
|| typeof proposeAllowed !== "boolean"
|| typeof confirmationAllowed !== "boolean"
|| acceptanceAllowed !== selectionAllowed
|| (row.accept_allowed !== undefined
&& (typeof row.accept_allowed !== "boolean" || row.accept_allowed !== selectionAllowed))
|| (row.confirm_allowed !== undefined
&& (typeof row.confirm_allowed !== "boolean" || row.confirm_allowed !== confirmationAllowed))
|| (row.display_allowed !== undefined
&& (typeof row.display_allowed !== "boolean" || (selectionAllowed && !row.display_allowed)))
|| (proposeAllowed && !selectionAllowed)
|| (confirmationAllowed && !selectionAllowed)
) {
return CLOSED_ENGINE_CAPABILITY_CEILING;
}
return { acceptanceAllowed, selectionAllowed, proposeAllowed, confirmationAllowed };
}
export const RECTIFICATION_TERMINATION_COPY = "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。";
/** Standalone range-delivery floor; the exact-minute confirmation gate remains 4/3. */
@@ -98,8 +153,7 @@ export type DecideRectificationInput = Readonly<{
holdoutValidation?: HoldoutValidationStatus;
accepted?: boolean;
inferenceCredibleRange?: readonly [string, string] | null;
engineAcceptAllowed?: boolean;
engineProposeAllowed?: boolean;
engineCeiling: EngineCapabilityCeiling;
datedMethodCollectOpen?: boolean;
/** Server-persisted inference counters; never infer these from chat text. */
inferenceRounds?: number;
@@ -111,28 +165,57 @@ export type DecideRectificationInput = Readonly<{
userUncertaintyHigh?: boolean;
}>;
function evidenceStopReason(
function classifyStop(
input: DecideRectificationInput,
separation: CandidateSeparation,
): EvidenceStopReason | null {
if (separation.ranked.length === 0) return null;
): StopClass | null {
if (
input.datedEventCount !== undefined
&& input.datedEventCount < MIN_STANDALONE_DATED_EVENTS
) {
return "insufficient_dated_events";
return { kind: "keep_collecting", reason: "insufficient_dated_events" };
}
if (
input.datedDomainCount !== undefined
&& input.datedDomainCount < MIN_STANDALONE_DATED_DOMAINS
) {
return "insufficient_domains";
return { kind: "keep_collecting", reason: "insufficient_domains" };
}
if (separation.ranked.length === 0 && !input.discriminatorProbe) {
return { kind: "keep_collecting", reason: "insufficient_dated_events" };
}
if (input.userStopped === true) return { kind: "user_stopped" };
if (separation.tiedForFirst) return { kind: "exhausted", reason: "tied_first" };
if (input.userUncertaintyHigh === true) {
return { kind: "exhausted", reason: "user_uncertainty_too_high" };
}
if (separation.tiedForFirst) return "tied_first";
if (input.userUncertaintyHigh === true) return "user_uncertainty_too_high";
return null;
}
function deliveryCapability(input: {
stopClass: StopClass | null;
separation: CandidateSeparation;
holdout: HoldoutValidationStatus;
engineCeiling: EngineCapabilityCeiling;
confirmationAllowed: boolean;
coverageBlocks: boolean;
}): DeliveryCapability {
const locallySelectable = input.separation.ranked.length > 0
&& !input.coverageBlocks
&& input.stopClass?.kind !== "keep_collecting"
&& input.stopClass?.kind !== "exhausted"
&& input.separation.sufficient
&& input.holdout === "passed";
return {
canAdopt: locallySelectable && input.engineCeiling.acceptanceAllowed,
selectionAllowed: locallySelectable && input.engineCeiling.selectionAllowed,
proposeAllowed: locallySelectable && input.engineCeiling.proposeAllowed,
canConfirmExactMinute: locallySelectable
&& input.confirmationAllowed
&& input.engineCeiling.confirmationAllowed,
};
}
export function decideRectification(input: DecideRectificationInput): RectificationDecision {
const separation = evaluateCandidateSeparation(input.candidateScores);
const probe = input.discriminatorProbe ?? null;
@@ -144,9 +227,63 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
?? (separation.representativeTime
? [separation.representativeTime, separation.representativeTime] as const
: null);
const stopClass = classifyStop(input, separation);
const stopReason = stopClass && "reason" in stopClass ? stopClass.reason : null;
const canDiscriminateDespiteCoverage = Boolean(probe)
&& !separation.sufficient
&& input.trainingGateOpen !== false;
const coverageBlocks = (!input.methodCoverageAll && !canDiscriminateDespiteCoverage)
|| input.trainingGateOpen === false;
const capability = deliveryCapability({
stopClass,
separation,
holdout,
engineCeiling: input.engineCeiling,
confirmationAllowed,
coverageBlocks,
});
const stopReason = evidenceStopReason(input, separation);
if (input.snapshotCurrent === false) {
if (probe && !userStopped && input.trainingGateOpen !== false) {
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
}
return collect(separation, holdout, range, probe, capability, stopReason);
}
if (userStopped && separation.ranked.length > 0) {
return completeWithRange(separation, holdout, range, "user_stopped", capability);
}
if (coverageBlocks) {
const engineOffers = input.engineCeiling.acceptanceAllowed
|| input.engineCeiling.proposeAllowed;
if (
stopClass?.kind !== "keep_collecting"
&& input.trainingGateOpen !== false
&& separation.ranked.length > 0
&& !probe
&& engineOffers
&& input.datedMethodCollectOpen !== true
) {
return offerRangeWithoutAdopt(separation, holdout, range, capability);
}
return collect(separation, holdout, range, probe, capability, stopReason);
}
if (stopClass?.kind === "keep_collecting") {
return collect(separation, holdout, range, probe, capability, stopClass.reason);
}
if (stopClass?.kind === "exhausted" && stopClass.reason === "user_uncertainty_too_high") {
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
}
if (!separation.sufficient) {
if (probe) {
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
}
return stopClass?.kind === "exhausted"
? completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason)
: completeWithRange(separation, holdout, range, "offer", capability);
}
if (stopClass?.kind === "exhausted") {
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
}
if (input.accepted) {
return finish(confirmationAllowed ? "awaiting_confirmation" : "adopt_representative", {
input,
@@ -154,7 +291,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
holdout,
range,
probe: null,
canConfirmExactMinute: confirmationAllowed,
capability,
});
}
if (confirmationAllowed) {
@@ -164,78 +301,20 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
holdout,
range,
probe: null,
canConfirmExactMinute: true,
capability,
});
}
if (userStopped && input.candidateScores.length > 0) {
return completeWithRange(separation, holdout, range, "user_stopped");
}
if (input.snapshotCurrent === false) {
if (probe && !userStopped && input.trainingGateOpen !== false) {
return discriminateOrExhaust(
input,
separation,
holdout,
range,
probe,
);
}
if (!(userStopped && input.candidateScores.length > 0)) {
return collect(separation, holdout, range, probe);
}
}
if (stopReason) {
return completeWithRange(separation, holdout, range, "exhausted", stopReason);
}
const canDiscriminateDespiteCoverage = Boolean(probe)
&& !separation.sufficient
&& input.trainingGateOpen !== false;
const coverageBlocks = (!input.methodCoverageAll && !canDiscriminateDespiteCoverage)
|| input.trainingGateOpen === false;
if (coverageBlocks) {
const trainingOpen = input.trainingGateOpen !== false;
const engineOffers = input.engineAcceptAllowed === true || input.engineProposeAllowed === true;
if (
trainingOpen
&& input.candidateScores.length > 0
&& !probe
&& engineOffers
&& input.datedMethodCollectOpen !== true
) {
return offerRangeWithoutAdopt(separation, holdout, range);
}
return collect(separation, holdout, range, probe);
}
if (!separation.sufficient) {
if (probe && !userStopped) {
return discriminateOrExhaust(input, separation, holdout, range, probe);
}
return completeWithRange(separation, holdout, range, userStopped ? "user_stopped" : "offer");
}
if (holdout === "not_started" && !userStopped) {
return holdoutValidation(separation, range);
if (holdout === "not_started") {
return holdoutValidation(separation, range, capability);
}
if (holdout === "failed") {
if (probe && !userStopped) {
return discriminateOrExhaust(input, separation, holdout, range, probe);
if (probe) {
return discriminateOrExhaust(input, separation, holdout, range, probe, capability);
}
return completeWithRange(separation, holdout, range, "exhausted");
return completeWithRange(separation, holdout, range, "exhausted", capability);
}
if (holdout === "unavailable") {
if (userStopped) {
return completeWithRange(separation, holdout, range, "user_stopped");
}
return finish("adopt_representative", {
input,
separation,
holdout,
range,
probe: null,
canConfirmExactMinute: false,
});
}
if (userStopped && holdout !== "passed") {
return completeWithRange(separation, holdout, range, "user_stopped");
return offerRangeWithoutAdopt(separation, holdout, range, capability);
}
return finish("adopt_representative", {
input,
@@ -243,7 +322,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
holdout,
range,
probe: null,
canConfirmExactMinute: false,
capability,
});
}
@@ -259,11 +338,13 @@ function discriminateOrExhaust(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
probe: CandidateDiscriminatorProbe,
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
if (budgetExhausted(input)) {
return completeWithRange(separation, holdout, range, "exhausted");
return completeWithRange(separation, holdout, range, "exhausted", capability, stopReason);
}
return discriminate(separation, holdout, range, probe);
return discriminate(separation, holdout, range, probe, capability, stopReason);
}
function collect(
@@ -271,6 +352,8 @@ function collect(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
probe: CandidateDiscriminatorProbe | null,
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
return {
phase: "event_collection",
@@ -278,10 +361,7 @@ function collect(
sessionOutcome: "collect_evidence",
resultStatus: "insufficient_evidence",
canOfferRange: false,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "collect_events",
activeFocusPolicy: "keep",
completionStatus: null,
@@ -292,6 +372,7 @@ function collect(
probe,
holdoutValidation: holdout,
droppedProbes: [],
stopReason,
};
}
@@ -299,6 +380,7 @@ function offerRangeWithoutAdopt(
separation: CandidateSeparation,
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
capability: DeliveryCapability,
): RectificationDecision {
return {
phase: "discrimination",
@@ -306,10 +388,7 @@ function offerRangeWithoutAdopt(
sessionOutcome: "provisional_range",
resultStatus: "insufficient_evidence",
canOfferRange: true,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "theme_refine",
activeFocusPolicy: "close",
completionStatus: null,
@@ -361,6 +440,8 @@ function discriminate(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
probe: CandidateDiscriminatorProbe,
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
return {
phase: "discrimination",
@@ -368,10 +449,7 @@ function discriminate(
sessionOutcome: "discriminate_candidates",
resultStatus: "discriminating",
canOfferRange: false,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "theme_refine",
activeFocusPolicy: "keep",
completionStatus: null,
@@ -382,12 +460,14 @@ function discriminate(
probe,
holdoutValidation: holdout,
droppedProbes: [],
...(stopReason ? { stopReason } : {}),
};
}
function holdoutValidation(
separation: CandidateSeparation,
range: readonly [string, string] | null,
capability: DeliveryCapability,
): RectificationDecision {
return {
phase: "holdout_validation",
@@ -395,10 +475,7 @@ function holdoutValidation(
sessionOutcome: "validate_holdout",
resultStatus: "discriminating",
canOfferRange: false,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "theme_refine",
activeFocusPolicy: "keep",
completionStatus: null,
@@ -417,6 +494,7 @@ function completeWithRange(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
kind: "user_stopped" | "offer" | "exhausted",
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
const userStopped = kind === "user_stopped";
@@ -433,10 +511,7 @@ function completeWithRange(
sessionOutcome,
resultStatus: "completed_with_range",
canOfferRange: true,
canAdopt: true,
canConfirmExactMinute: false,
selectionAllowed: true,
proposeAllowed: true,
...capability,
precisionStage: "ready_to_adopt",
activeFocusPolicy: "close",
completionStatus: userStopped ? "provisional_range_user_stopped" : null,
@@ -460,17 +535,17 @@ function finish(
holdout: HoldoutValidationStatus;
range: readonly [string, string] | null;
probe: CandidateDiscriminatorProbe | null;
canConfirmExactMinute: boolean;
capability: DeliveryCapability;
},
): RectificationDecision {
const completionStatus: CompletionStatus | null = input.canConfirmExactMinute && input.input.accepted
const completionStatus: CompletionStatus | null = input.capability.canConfirmExactMinute && input.input.accepted
? "exact_minute_confirmed"
: input.holdout === "passed"
? "validated_range"
: input.input.userStopped === true
? "provisional_range_user_stopped"
: null;
const kind = input.canConfirmExactMinute
const kind = input.capability.canConfirmExactMinute
? (input.input.accepted ? "exact_minute_confirmed" : "awaiting_confirmation")
: input.holdout === "passed"
? "validated_range"
@@ -483,10 +558,7 @@ function finish(
? "converged"
: "completed_with_range",
canOfferRange: true,
canAdopt: true,
canConfirmExactMinute: input.canConfirmExactMinute,
selectionAllowed: true,
proposeAllowed: true,
...input.capability,
precisionStage: "ready_to_adopt",
activeFocusPolicy: "close",
completionStatus,
@@ -679,7 +679,7 @@ ${nextInterview.hostNarration}`
nextInterviewPersisted = true;
}
}
const adoptionNarration = nextAction.type === "offer_provisional_range" && nextAction.can_adopt
const adoptionNarration = nextAction.can_offer_range && nextAction.can_adopt
? `${nonConvergingRangeNarration({
credibleRange: nextAction.credible_range,
representativeTime: nextAction.representative_time,
@@ -18,6 +18,7 @@ import {
} from "../core/candidate-contrast-packet.ts";
import {
decideRectification,
engineCapabilityCeilingFromReceipt,
publicDecisionFields,
type HoldoutValidationStatus,
type RectificationDecision,
@@ -522,9 +523,7 @@ export function decideFromDossier(
holdoutValidation: holdoutStatusFromInference(inference, oosBlindPrompts),
accepted: Boolean(dossier.case.acceptedTime),
inferenceCredibleRange: inference?.credible_range ?? null,
engineAcceptAllowed: latest?.selectionAllowed === true
|| latest?.decisionReceipt?.accept_allowed === true,
engineProposeAllowed: latest?.decisionReceipt?.propose_allowed === true,
engineCeiling: engineCapabilityCeilingFromReceipt(latest?.decisionReceipt ?? null),
datedMethodCollectOpen: datedMethodCollectOpen(collecting.methods),
...decisionBudget,
...evidenceStops,
@@ -553,6 +552,7 @@ export function decideAfterInferenceChange(input: {
trainingGateOpen: trainingScoreableGate(input.dossier.evidence).open,
candidateScores: [],
userStopped: input.userStopped,
engineCeiling: engineCapabilityCeilingFromReceipt(input.dossier.latestResult?.decisionReceipt ?? null),
...decisionBudgetFromInference(null),
...evidenceStops,
});
@@ -593,9 +593,7 @@ export function decideAfterInferenceChange(input: {
inferenceCredibleRange: input.state.credible_range,
userStopped: input.userStopped,
accepted: Boolean(input.dossier.case.acceptedTime),
engineAcceptAllowed: input.dossier.latestResult?.selectionAllowed === true
|| input.dossier.latestResult?.decisionReceipt?.accept_allowed === true,
engineProposeAllowed: input.dossier.latestResult?.decisionReceipt?.propose_allowed === true,
engineCeiling: engineCapabilityCeilingFromReceipt(input.dossier.latestResult?.decisionReceipt ?? null),
datedMethodCollectOpen: datedMethodCollectOpen(collecting.methods),
...decisionBudgetFromInference(input.state),
...evidenceStops,
@@ -616,9 +614,22 @@ export function overlayPublicDecision<T extends object>(
const fields = publicDecisionFields(decision);
const projection = authoritativeCandidateProjection(snapshot as DecisionDossier["latestResult"]);
const inconsistent = !projection.consistent;
const ceiling = engineCapabilityCeilingFromReceipt(
(snapshot as DecisionDossier["latestResult"])?.decisionReceipt ?? null,
);
const canAdopt = !inconsistent && fields.can_adopt && ceiling.acceptanceAllowed;
const selectionAllowed = !inconsistent && fields.selection_allowed && ceiling.selectionAllowed;
const proposeAllowed = !inconsistent && fields.propose_allowed && ceiling.proposeAllowed;
const canConfirmExactMinute = !inconsistent
&& fields.can_confirm_exact_minute
&& ceiling.confirmationAllowed;
return {
...snapshot,
...fields,
can_adopt: canAdopt,
selection_allowed: selectionAllowed,
propose_allowed: proposeAllowed,
can_confirm_exact_minute: canConfirmExactMinute,
...(projection.fromInference
? {
candidates: projection.candidates,
@@ -628,8 +639,7 @@ export function overlayPublicDecision<T extends object>(
credible_range: projection.credibleRange,
}
: {}),
...(inconsistent ? { can_adopt: false, selection_allowed: false } : {}),
selectionAllowed: inconsistent ? false : fields.selection_allowed,
selectionAllowed,
validated: fields.validated,
completionStatus: fields.completion_status,
};
@@ -70,7 +70,9 @@ export type V9EngineScoreResult = Readonly<{
candidates: readonly V9EngineCandidate[];
overallConfidence: "low" | "medium" | "high";
marginPercent: number | null;
acceptanceAllowed: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
confirmationAllowed: boolean;
representativeCandidateId: string | null;
representativeTime: string | null;
@@ -237,7 +239,9 @@ type ParsedReceipt = Readonly<{
raw: V9DecisionReceipt;
eventContractVersion: string;
policyVersion: string;
acceptanceAllowed: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
confirmationAllowed: boolean;
representativeCandidateId: string | null;
representativeTime: string | null;
@@ -267,6 +271,7 @@ function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandid
|| typeof row.display_allowed !== "boolean"
|| typeof row.selection_allowed !== "boolean"
|| typeof row.acceptance_allowed !== "boolean"
|| typeof row.propose_allowed !== "boolean"
|| typeof row.confirmation_allowed !== "boolean"
|| typeof row.accept_allowed !== "boolean"
|| typeof row.confirm_allowed !== "boolean"
@@ -288,7 +293,6 @@ function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandid
|| (representativeCandidateId !== null && (!representative || representative.time !== representativeTime))
|| (row.confirmation_allowed === true && row.selection_allowed !== true)
|| (row.selection_allowed === true && row.display_allowed !== true)
|| (row.propose_allowed !== undefined && typeof row.propose_allowed !== "boolean")
|| (row.propose_allowed === true && row.selection_allowed !== true)
|| !questionContractVersionIsCompatible(row.question_contract_version ?? row.question_contract)
) {
@@ -298,7 +302,9 @@ function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandid
raw: row,
eventContractVersion: EVENT_CONTRACT_VERSION,
policyVersion: row.policy_version,
acceptanceAllowed: row.acceptance_allowed,
selectionAllowed: row.selection_allowed,
proposeAllowed: row.propose_allowed,
confirmationAllowed: row.confirmation_allowed,
representativeCandidateId,
representativeTime,
@@ -527,7 +533,9 @@ export async function runV9CandidateScore(input: {
candidates,
overallConfidence: receipt.overallConfidence,
marginPercent: receipt.marginPercent,
acceptanceAllowed: receipt.acceptanceAllowed,
selectionAllowed: receipt.selectionAllowed,
proposeAllowed: receipt.proposeAllowed,
confirmationAllowed: receipt.confirmationAllowed,
representativeCandidateId: receipt.representativeCandidateId,
representativeTime: receipt.representativeTime,
@@ -1139,8 +1139,14 @@ export function decideConversationalSession(input: {
: discriminatorFromFollowup(input.nextFollowup),
holdoutValidation: input.holdoutValidation,
accepted: input.accepted,
engineAcceptAllowed: input.selectionAllowed,
engineProposeAllowed: input.proposeAllowed,
// This helper is not used by the production V9 delivery path; keep its
// delivery ceiling explicit without coupling it to question selection.
engineCeiling: {
acceptanceAllowed: input.selectionAllowed === true,
selectionAllowed: input.selectionAllowed === true,
proposeAllowed: input.proposeAllowed === true,
confirmationAllowed: input.confirmationAllowed === true,
},
datedMethodCollectOpen: input.methods ? datedMethodCollectOpen(input.methods) : undefined,
inferenceRounds: input.inferenceRounds,
effectiveAnswerCount: input.effectiveAnswerCount,
@@ -26,7 +26,7 @@ import {
userFacingRunFailure,
isIncompleteRunBanner,
} from "../src/lib/rectification-agentic/v9/run-diagnostic.ts";
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { applyHoldoutAnswer, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { RECTIFICATION_TERMINATION_COPY } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { parseV9CaseDossier, RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
@@ -425,11 +425,11 @@ function familyCollectDossier() {
}
function adoptionInference() {
return buildInferenceState({
return applyHoldoutAnswer(buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 16 },
{ id: "05:00", time: "05:00", relative_support: 18 },
{ id: "05:10", time: "05:10", relative_support: 14 },
],
events: [
@@ -439,7 +439,7 @@ function adoptionInference() {
{ id: "e4", domain: "relationship", year: 2024, precision: "day" },
],
probes: [RELOCATION_2015_PROBE],
});
}), "yes");
}
function adoptionDossier() {
@@ -466,7 +466,13 @@ function adoptionDossier() {
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "05:00",
decisionReceipt: { inference_state: inference },
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: inference,
},
}),
conversationSummary: conversationSummaryFixture({
declinedSkippedTopics: [{ target_domain: "family", status: "skipped" }],
@@ -598,7 +604,8 @@ test("last structured choice emits the adoption range and persists the same narr
optionId: "A",
expectedRevision: adoptionInference().revision,
});
assert.equal(applied.nextAction.type, "offer_provisional_range");
// Formerly offer_provisional_range: that depended on a 16/14 unresolved fixture being adoptable.
assert.equal(applied.nextAction.type, "ready_to_adopt");
assert.equal(applied.nextAction.can_adopt, true);
assert.match(applied.narration, /当前可信区间是/);
assert.match(applied.narration, /代表分钟/);
@@ -820,9 +820,11 @@ function liveCaseDossier(): DecisionDossier {
})),
representativeTime: state.representative_time,
decisionReceipt: {
acceptance_allowed: true,
accept_allowed: true,
propose_allowed: true,
selection_allowed: true,
confirmation_allowed: false,
inference_state: state,
},
},
@@ -19,6 +19,7 @@ import {
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
import { safeToolErrorCode, RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
CANDIDATE_ID,
RESULT_ID,
@@ -138,6 +139,7 @@ const PLATEAU = [
function collectDecision(candidates: readonly { time: string; relativeSupport: number }[]) {
return decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
@@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import test from "node:test";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
import { EFFECTIVE_ANSWER_SAFETY_CAP } from "../src/lib/birth-time-dynamic-stop-policy.ts";
import {
DEFAULT_MAX_DISCRIMINATION_ROUNDS,
@@ -50,6 +52,7 @@ const PROBE: CandidateDiscriminatorProbe = {
};
const BASE_INPUT = {
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [
@@ -165,7 +168,8 @@ test("exhausted discrimination still delivers the credible candidate range", ()
assert.equal(decision.resultStatus, "completed_with_range");
assert.equal(decision.sessionOutcome, "completed_with_range");
assert.equal(decision.canOfferRange, true);
assert.equal(decision.canAdopt, true);
// 原值是 true;候选未分开且区分预算耗尽只能交付 review-only range,不能采用。
assert.equal(decision.canAdopt, false);
assert.deepEqual(decision.credibleRange, ["05:00", "05:07"]);
});
@@ -198,19 +202,24 @@ test("additional score evidence never widens the credible range", () => {
assert.ok(width(after.credibleRange) <= width(before.credibleRange));
});
test("evidence-state stops complete with a usable range and the fixed termination copy", () => {
test("insufficient standalone evidence keeps collecting", () => {
for (const item of [
{ reason: "insufficient_dated_events" as const, input: { datedEventCount: 2, datedDomainCount: 2 } },
{ reason: "insufficient_domains" as const, input: { datedEventCount: 3, datedDomainCount: 1 } },
]) {
const decision = decideRectification({ ...BASE_INPUT, ...item.input });
assert.equal(decision.nextAction, "ask_fact_collection");
assert.equal(decision.sessionOutcome, "collect_evidence");
assert.equal(decision.canOfferRange, false);
assert.equal(decision.stopReason, item.reason);
}
});
test("exhausted evidence-state stops complete with a usable range and the fixed termination copy", () => {
const cases: readonly Readonly<{
reason: EvidenceStopReason;
input: Partial<Parameters<typeof decideRectification>[0]>;
}>[] = [
{
reason: "insufficient_dated_events",
input: { datedEventCount: 2, datedDomainCount: 2 },
},
{
reason: "insufficient_domains",
input: { datedEventCount: 3, datedDomainCount: 1 },
},
{
reason: "tied_first",
input: {
@@ -221,6 +230,7 @@ test("evidence-state stops complete with a usable range and the fixed terminatio
{ time: "05:06", score: 34 },
{ time: "05:07", score: 32 },
],
discriminatorProbe: null,
},
},
{
@@ -393,11 +403,13 @@ test("dossier and post-inference decisions share the half-uncertain stop rule",
test("rectification label ladder follows evidence state and explicit adapter support", () => {
const blocked = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: [],
});
assert.equal(rectificationLabel({ decision: blocked }), "blocked");
// 原值是 blocked;空候选现在被分类为证据不足,不能继续无原因地 fail-open。
assert.equal(rectificationLabel({ decision: blocked }), "user_history_verification_required");
const stopped = decideRectification({
...BASE_INPUT,
@@ -94,7 +94,13 @@ function producedSnapshot(
candidates: persisted,
representativeTime: state.representative_time,
evidenceLedgerFingerprint: evidenceLedgerFingerprint(COVERAGE_EVIDENCE as never),
decisionReceipt: { inference_state: state },
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: state,
},
},
};
}
@@ -128,7 +134,7 @@ test("producer 58/42 lead keeps both candidates and can close without opening th
assert.notEqual(decision.sessionOutcome, "exact_minute_confirmed");
});
test("producer 58/42 with no holdout is ready_to_adopt, not a unique minute", () => {
test("producer 58/42 with no holdout remains a review-only range", () => {
const { latest } = producedSnapshot([
{ time: "05:02", relativeSupport: 58 },
{ time: "04:55", relativeSupport: 42 },
@@ -136,7 +142,9 @@ test("producer 58/42 with no holdout is ready_to_adopt, not a unique minute", ()
const decision = decideFromDossier(dossierFor(latest));
assert.equal(authoritativeCandidateProjection(latest).consistent, true);
assert.equal(decision.separation.sufficient, true);
assert.equal(decision.nextAction, "ready_to_adopt");
// 原值是 ready_to_adopt;没有 holdout 不能进入采用态,只能展示 review-only range。
assert.equal(decision.nextAction, "offer_provisional_range");
assert.equal(decision.canAdopt, false);
assert.equal(decision.canConfirmExactMinute, false);
assert.notEqual(decision.sessionOutcome, "exact_minute_confirmed");
});
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
import {
askedEventProbeKeysFromLedgerEvidence,
buildCandidateContrastPacket,
@@ -128,12 +130,14 @@ test("unseparated scores still discriminate when training is open and a probe ex
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).sessionOutcome, "discriminate_candidates");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: [],
@@ -302,6 +306,7 @@ test("user stop skips remaining collection and holdout when candidates already e
});
assert.equal(separated.type, "complete_with_range");
const stopped = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
userStopped: true,
candidateScores: [
@@ -364,6 +369,7 @@ test("discrimination waits for three training events after reserving holdout", (
test("holdout passed is a validated range, not a user-stop close", () => {
const validated = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
candidateScores: [
{ time: "04:48", score: 58 },
@@ -473,6 +479,7 @@ test("stale snapshot keeps discrimination instead of returning to collection", (
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: false,
@@ -494,12 +501,15 @@ test("a single candidate is sole_candidate, not a parallel range", () => {
assert.equal(separation.lead, 0);
assert.deepEqual(separation.credibleRange, ["05:00"]);
const decision = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [{ time: "05:00", score: 80 }],
holdoutValidation: "unavailable",
});
assert.equal(decision.sessionOutcome, "adopt_representative");
// 原值是 adopt_representative;holdout unavailable 且用户未停止只能提供 review-only range。
assert.equal(decision.sessionOutcome, "provisional_range");
assert.equal(decision.canAdopt, false);
assert.equal(decision.canConfirmExactMinute, false);
assert.doesNotMatch(
buildSkillVerificationReport({
@@ -4,6 +4,7 @@ import test from "node:test";
import {
decideRectification,
engineCapabilityCeilingFromReceipt,
publicDecisionFields,
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-next-action.ts";
@@ -29,6 +30,182 @@ const SEPARATED = [
{ time: "04:49", score: 42 },
];
type TestEngineCapabilityCeiling = Readonly<{
acceptanceAllowed: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
confirmationAllowed: boolean;
}>;
const ENGINE_OPEN: TestEngineCapabilityCeiling = {
acceptanceAllowed: true,
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: true,
};
function capabilityCombinations(): TestEngineCapabilityCeiling[] {
return Array.from({ length: 16 }, (_, bits) => ({
acceptanceAllowed: Boolean(bits & 1),
selectionAllowed: Boolean(bits & 2),
proposeAllowed: Boolean(bits & 4),
confirmationAllowed: Boolean(bits & 8),
}));
}
function decideWithEngineCeiling(
engineCeiling: TestEngineCapabilityCeiling,
overrides: Partial<Parameters<typeof decideRectification>[0]> = {},
) {
return decideRectification({
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: true,
candidateScores: SEPARATED,
holdoutValidation: "passed",
confirmationAllowed: true,
datedEventCount: 3,
datedDomainCount: 2,
...overrides,
engineCeiling,
} as Parameters<typeof decideRectification>[0]);
}
test("invariant 1: TypeScript delivery capabilities never exceed the engine ceiling", () => {
for (const ceiling of capabilityCombinations()) {
const decision = decideWithEngineCeiling(ceiling);
assert.equal(decision.canAdopt && !ceiling.acceptanceAllowed, false, JSON.stringify(ceiling));
assert.equal(decision.selectionAllowed && !ceiling.selectionAllowed, false, JSON.stringify(ceiling));
assert.equal(decision.proposeAllowed && !ceiling.proposeAllowed, false, JSON.stringify(ceiling));
assert.equal(decision.canConfirmExactMinute && !ceiling.confirmationAllowed, false, JSON.stringify(ceiling));
}
});
test("invariant 2: insufficient dated evidence never permits selection or adoption", () => {
for (const datedEventCount of [0, 2, 3]) {
for (const datedDomainCount of [0, 1, 2]) {
if (datedEventCount >= 3 && datedDomainCount >= 2) continue;
const decision = decideWithEngineCeiling(ENGINE_OPEN, {
confirmationAllowed: false,
datedEventCount,
datedDomainCount,
});
assert.equal(decision.canAdopt, false, `${datedEventCount} events / ${datedDomainCount} domains`);
assert.equal(decision.selectionAllowed, false, `${datedEventCount} events / ${datedDomainCount} domains`);
}
}
});
test("invariant 3: tied leading candidates remain review-only when the user stops", () => {
const tiedScores = [
{ time: "04:48", score: 12 },
{ time: "04:49", score: 12 },
{ time: "04:50", score: 11 },
];
const stopped = decideWithEngineCeiling(ENGINE_OPEN, {
candidateScores: tiedScores,
confirmationAllowed: false,
userStopped: true,
});
assert.equal(stopped.completionStatus, "provisional_range_user_stopped");
assert.equal(stopped.canAdopt, false);
const continuing = decideWithEngineCeiling(ENGINE_OPEN, {
candidateScores: tiedScores,
confirmationAllowed: false,
userStopped: false,
});
assert.equal(continuing.separation.tiedForFirst, true);
assert.equal(continuing.canAdopt, false);
});
test("invariant 4: unavailable holdout stays review-only whether or not the user stops", () => {
for (const userStopped of [false, true]) {
const decision = decideWithEngineCeiling(ENGINE_OPEN, {
confirmationAllowed: false,
holdoutValidation: "unavailable",
userStopped,
});
assert.equal(decision.canAdopt, false, `userStopped=${userStopped}`);
}
});
test("raw engine receipt contradictions fail closed before delivery", () => {
const openReceipt = {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
};
const closed = {
acceptanceAllowed: false,
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: false,
};
for (const receipt of [
{ ...openReceipt, accept_allowed: false },
{ ...openReceipt, confirm_allowed: true },
{ ...openReceipt, display_allowed: false },
{ ...openReceipt, confirmation_allowed: undefined },
]) {
assert.deepEqual(engineCapabilityCeilingFromReceipt(receipt), closed);
}
});
test("invariant 5: public overlay intersects all delivery gates with the engine receipt", () => {
const decision = decideWithEngineCeiling(ENGINE_OPEN);
const candidates = [
{ candidateId: "c-0507", time: "05:07", rank: 1, relativeSupport: 13, tiedMinuteCount: 1 },
{ candidateId: "c-0500", time: "05:00", rank: 2, relativeSupport: 12, tiedMinuteCount: 1 },
{ candidateId: "c-0515", time: "05:15", rank: 3, relativeSupport: 10, tiedMinuteCount: 1 },
];
const inferenceState = {
algorithm_version: "rectification-inference-v1",
candidate_set_id: "05:00-05:15:05:00,05:07,05:15",
revision: 2,
phase: "discrimination",
result_status: "discriminating",
range_start: "05:00",
range_end: "05:15",
candidates: [
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 13, posterior_score: 15, probability: 0.4, status: "active", rank: 2, strong_conflict_count: 0 },
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 12, posterior_score: 20, probability: 0.6, status: "active", rank: 1, strong_conflict_count: 0 },
{ id: "05:15", time: "05:15", cluster_range: ["05:15", "05:15"], prior_score: 10, posterior_score: 30, probability: 0, status: "eliminated", rank: 3, strong_conflict_count: 1 },
],
events: [],
probes: [],
answered_probes: [],
rounds: [],
entropy: 0.67,
representative_time: "05:00",
credible_range: ["05:00", "05:07"],
};
const validEngineCeilings = capabilityCombinations().filter((ceiling) => (
ceiling.acceptanceAllowed === ceiling.selectionAllowed
&& (!ceiling.proposeAllowed || ceiling.selectionAllowed)
&& (!ceiling.confirmationAllowed || ceiling.selectionAllowed)
));
for (const ceiling of validEngineCeilings) {
const overlaid = overlayPublicDecision({
candidates,
representativeTime: "05:07",
decisionReceipt: {
acceptance_allowed: ceiling.acceptanceAllowed,
selection_allowed: ceiling.selectionAllowed,
propose_allowed: ceiling.proposeAllowed,
confirmation_allowed: ceiling.confirmationAllowed,
inference_state: inferenceState,
},
}, decision);
assert.equal(overlaid.can_adopt, ceiling.acceptanceAllowed, JSON.stringify(ceiling));
assert.equal(overlaid.selection_allowed, ceiling.selectionAllowed, JSON.stringify(ceiling));
assert.equal(overlaid.propose_allowed, ceiling.proposeAllowed, JSON.stringify(ceiling));
assert.equal(overlaid.can_confirm_exact_minute, ceiling.confirmationAllowed, JSON.stringify(ceiling));
}
});
function readSource(relative: string) {
return readFileSync(new URL(relative, import.meta.url), "utf8");
}
@@ -63,7 +240,7 @@ test("public decision fields are derived from decideRectification", () => {
},
];
for (const input of fixtures) {
const decision = decideRectification(input);
const decision = decideRectification({ ...input, engineCeiling: ENGINE_OPEN });
const fields = publicDecisionFields(decision);
assert.deepEqual(fields, {
type: decision.nextAction,
@@ -518,6 +695,7 @@ test("career and relationship training still discriminates before family or occu
test("public candidate cards follow the inference ranking and hide an inconsistent state", () => {
const decision = decideRectification({
engineCeiling: ENGINE_OPEN,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: SEPARATED,
@@ -30,6 +30,7 @@ import {
resolveExactSkillPackage,
} from "../src/lib/skill-package-registry.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
CANDIDATE_ID,
FOCUS_ID,
@@ -55,6 +56,7 @@ const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
function collectDecision(candidates: readonly { time: string; relativeSupport?: number }[]) {
return decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport ?? 0 })),
@@ -198,6 +200,7 @@ const ENGINE_SCORE = {
display_allowed: true,
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
accept_allowed: true,
confirm_allowed: false,
@@ -856,6 +859,7 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
policy_version: "rectification-candidate-policy-v2",
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
representative_candidate_id: CANDIDATE_ID,
overall_confidence: "medium",
@@ -1373,6 +1377,15 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: producedInferenceState(plateauCandidates.map((item) => ({
id: item.candidateId,
time: item.time,
relative_support: item.relativeSupport,
}))),
execution_ledger: [
{ method: "vimshottari-dasha", status: "executed" },
{ method: "narayana-dasha", status: "executed" },
@@ -1389,7 +1402,7 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
// The execution ledger does not prove same-range support or raw-result agreement.
assert.equal(plateau.rectification_label, "manual_pattern_consensus");
assert.deepEqual(plateau.executed_methods, ["vimshottari-dasha", "narayana-dasha"]);
const completed = latestResultToolProjection({
const collecting = latestResultToolProjection({
resultId: RESULT_ID,
candidates: plateauCandidates,
selectionAllowed: true,
@@ -1405,6 +1418,7 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
],
},
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
datedEventCount: 2,
@@ -1414,10 +1428,41 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
score: item.relativeSupport,
})),
}));
assert.equal(completed.rectification_label, "user_history_verification_required");
assert.equal(completed.evidence_stop_reason, "insufficient_dated_events");
assert.equal(collecting.rectification_label, "user_history_verification_required");
assert.equal(collecting.evidence_stop_reason, "insufficient_dated_events");
assert.deepEqual(collecting.executed_methods, ["vimshottari-dasha", "narayana-dasha"]);
const exhaustedCandidates = plateauCandidates.map((item, index) => ({
...item,
relativeSupport: index < 2 ? 40 : 20,
}));
const completed = latestResultToolProjection({
resultId: RESULT_ID,
candidates: exhaustedCandidates,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
},
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
datedEventCount: 3,
datedDomainCount: 2,
candidateScores: exhaustedCandidates.map((item) => ({
time: item.time,
score: item.relativeSupport,
})),
}));
assert.equal(completed.termination_copy, "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。");
assert.deepEqual(completed.executed_methods, ["vimshottari-dasha", "narayana-dasha"]);
const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8");
assert.match(skill, /method_followup_plan/);
assert.match(skill, /感情 → 事业 → 家人 → 职业 → 占问/);
@@ -1435,12 +1480,12 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
test("Mastra hides active candidates when the receipt range is corrupted", () => {
const candidates = [
{ candidateId: CANDIDATE_ID, time: "05:00", rank: 1, relativeSupport: 20, tiedMinuteCount: 1 },
{ candidateId: SECOND_CANDIDATE_ID, time: "05:07", rank: 2, relativeSupport: 15, tiedMinuteCount: 1 },
{ candidateId: CANDIDATE_ID, time: "05:00", rank: 1, relativeSupport: 58, tiedMinuteCount: 1 },
{ candidateId: SECOND_CANDIDATE_ID, time: "05:07", rank: 2, relativeSupport: 42, tiedMinuteCount: 1 },
];
const inferenceState = producedInferenceState([
{ id: CANDIDATE_ID, time: "05:00", relative_support: 20 },
{ id: SECOND_CANDIDATE_ID, time: "05:07", relative_support: 15 },
{ id: CANDIDATE_ID, time: "05:00", relative_support: 58 },
{ id: SECOND_CANDIDATE_ID, time: "05:07", relative_support: 42 },
]);
const latest = {
resultId: RESULT_ID,
@@ -1451,9 +1496,16 @@ test("Mastra hides active candidates when the receipt range is corrupted", () =>
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: { inference_state: inferenceState },
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: inferenceState,
},
};
const session = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
userStopped: true,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
@@ -1468,6 +1520,7 @@ test("Mastra hides active candidates when the receipt range is corrupted", () =>
const invalid = latestResultToolProjection({
...latest,
decisionReceipt: {
...latest.decisionReceipt,
inference_state: { ...inferenceState, credible_range: ["04:00", "04:10"] },
},
}, session);
@@ -2896,11 +2949,15 @@ test("paused case with selection_allowed may offer the escape hatch", async () =
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: "paused",
evidence: [educationEvidence],
evidence: [educationEvidence, familyEvidence, careerEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence([educationEvidence]),
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence([
educationEvidence,
familyEvidence,
careerEvidence,
]),
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
@@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import test from "node:test";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
import { applyChoiceWithoutEvidence } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { applyHoldoutAnswer, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { unionStillValidRange } from "../src/lib/rectification-agentic/core/credible-range.ts";
@@ -155,6 +157,7 @@ test("hidden case walks collection through holdout to a range or representative
score: item.posterior_score,
}));
const afterAnswers = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
candidateScores: scores,
holdoutValidation: "not_started",
@@ -166,6 +169,7 @@ test("hidden case walks collection through holdout to a range or representative
assert.equal(holdoutPassed.holdout_passed, true);
assert.deepEqual(posteriorMap(holdoutPassed.candidates), posteriorMap(answered.state.candidates));
const closed = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
candidateScores: holdoutPassed.candidates.map((item) => ({
time: item.time,
@@ -223,6 +227,7 @@ test("holdout failure returns to candidate discrimination", () => {
}],
});
const next = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
candidateScores: failed.candidates.map((item) => ({ time: item.time, score: item.posterior_score })),
discriminatorProbe: selectDiscriminatorProbe(retryPacket),
@@ -277,6 +282,7 @@ test("mutated years and domains do not keep asking exam-quality copy", () => {
test("user stop is an unvalidated range, not a holdout pass", () => {
const stopped = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
userStopped: true,
candidateScores: [
@@ -293,6 +299,7 @@ test("user stop is an unvalidated range, not a holdout pass", () => {
test("tied candidates without a high-information probe offer a range instead of inventing left/right", () => {
const next = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [
@@ -378,6 +385,7 @@ test("case A: two then three events keep collecting; four training events may di
})),
});
return decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: state.events.filter((item) => item.usage === "training").length >= 3
&& new Set(state.events.filter((item) => item.usage === "training").map((item) => item.domain)).size >= 2,
@@ -97,7 +97,13 @@ function producedDossier(
})),
representativeTime: state.representative_time,
evidenceLedgerFingerprint: evidenceLedgerFingerprint(evidence as never),
decisionReceipt: { inference_state: state },
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: state,
},
};
return {
state,
@@ -121,7 +127,7 @@ function holdoutFollowup(dossier: ReturnType<typeof producedDossier>["dossier"])
});
}
test("sticky holdout with unknown precision does not ask validation and can adopt", () => {
test("sticky holdout with unknown precision stays review-only", () => {
const { state, dossier: dated } = producedDossier(COVERED_EVIDENCE);
const holdout = state.events.find((item) => item.usage === "holdout");
assert.ok(holdout);
@@ -141,11 +147,14 @@ test("sticky holdout with unknown precision does not ask validation and can adop
const decision = decideFromDossier(dossier);
assert.notEqual(decision.nextAction, "ask_holdout_validation");
assert.equal(decision.nextAction, "ready_to_adopt");
assert.equal(decision.sessionOutcome, "adopt_representative");
// 原值是 ready_to_adopt;unknown precision 让 holdout 不可用,不能进入采用态。
assert.equal(decision.nextAction, "offer_provisional_range");
// 原值是 adopt_representative;未通过独立核对只能交付临时区间。
assert.equal(decision.sessionOutcome, "provisional_range");
assert.notEqual(decision.sessionOutcome, "validated_range");
assert.equal(decision.validated, false);
assert.equal(decision.canAdopt, true);
// 原值是 true;holdout unavailable 必须 fail-closed。
assert.equal(decision.canAdopt, false);
assert.equal(decision.canConfirmExactMinute, false);
});
@@ -169,7 +178,10 @@ test("passed holdout is a validated range, not a unique minute", () => {
...dossier,
latestResult: {
...dossier.latestResult,
decisionReceipt: { inference_state: passed },
decisionReceipt: {
...dossier.latestResult.decisionReceipt,
inference_state: passed,
},
},
});
assert.equal(decided.sessionOutcome, "validated_range");
@@ -187,7 +199,10 @@ test("failed holdout keeps existing non-validated close or discriminate path", (
...dossier,
latestResult: {
...dossier.latestResult,
decisionReceipt: { inference_state: failed },
decisionReceipt: {
...dossier.latestResult.decisionReceipt,
inference_state: failed,
},
},
});
assert.equal(decided.holdoutValidation, "failed");
@@ -21,6 +21,7 @@ import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
CANDIDATE_ID,
EVIDENCE_ID,
@@ -128,6 +129,7 @@ test("a 25-minute tied plateau projects width and forbids unique-minute confirma
selectionKind: null,
algorithmVersion: "rectification-v5",
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
@@ -309,12 +311,11 @@ test("training gate closed still withholds yearless varga cards", () => {
test("training complete with no renderable distinguish card allows offering a range without adopt", () => {
const blocked = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: NARROW_LEAD,
discriminatorProbe: null,
engineAcceptAllowed: true,
engineProposeAllowed: true,
});
assert.equal(blocked.canOfferRange, true);
assert.equal(blocked.canAdopt, false);
@@ -322,12 +323,11 @@ test("training complete with no renderable distinguish card allows offering a ra
assert.equal(blocked.nextAction, "offer_provisional_range");
assert.equal(blocked.sessionOutcome, "provisional_range");
const stillClosed = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: NARROW_LEAD,
discriminatorProbe: null,
engineAcceptAllowed: true,
engineProposeAllowed: true,
});
assert.equal(stillClosed.canOfferRange, false);
assert.equal(stillClosed.canAdopt, false);
@@ -352,8 +352,10 @@ test("decideFromDossier offers a range when training is complete and the ask lay
evidenceLedgerFingerprint: evidenceLedgerFingerprint(CASE_EVIDENCE as never),
decisionReceipt: {
accept_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
selection_allowed: true,
confirmation_allowed: false,
},
},
case: { acceptedTime: null },
@@ -440,6 +442,7 @@ test("a mismatched receipt house_table is not emitted with another representativ
test("tool projection does not pair a 05:00 house table with a 04:47 representative time", () => {
const decision = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: TIED,
@@ -32,6 +32,7 @@ import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
FOCUS_ID,
TURN_ID,
@@ -334,8 +335,10 @@ function liveDossier(
representativeTime: "04:51",
decisionReceipt: {
accept_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
selection_allowed: true,
confirmation_allowed: false,
inference_state: state,
...(extra.eventProbes
? {
@@ -401,8 +404,10 @@ function rpcDossier(decision: DecisionDossier) {
candidates: snapshotCandidates(),
decisionReceipt: {
accept_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
selection_allowed: true,
confirmation_allowed: false,
...(decision.latestResult?.decisionReceipt ?? {}),
},
}),
@@ -583,23 +588,24 @@ test("offerRangeWithoutAdopt persists a spoken collect and narrates the numeric
assert.equal(Boolean(idle.hostNarration) && Boolean(question), true);
});
test("userStopped still completes as provisional_range_user_stopped with canAdopt", () => {
test("userStopped completes with a review-only provisional range", () => {
const stopped = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
userStopped: true,
engineAcceptAllowed: true,
engineProposeAllowed: true,
candidateScores: ACTIVE.map((time) => ({ time, score: SCORES[time] ?? 0 })),
});
assert.equal(stopped.sessionOutcome, "provisional_range_user_stopped");
assert.equal(stopped.canAdopt, true);
// 原值是 true;用户停止只结束追问,不能绕过 coverage 和 holdout 门槛。
assert.equal(stopped.canAdopt, false);
assert.equal(stopped.canOfferRange, true);
assert.equal(isNonConvergingRangeOffer(stopped), false);
const dossier = liveDossier([CAREER_2024_04, CAREER_2023_ACTIVATION], { status: "paused" });
const fromDossier = decideFromDossier(dossier, { birthDate: "1997-08-08" });
assert.equal(fromDossier.sessionOutcome, "provisional_range_user_stopped");
assert.equal(fromDossier.canAdopt, true);
// 原值是 true;该 dossier 只有两个同域日期事件,停止不等于证据达标。
assert.equal(fromDossier.canAdopt, false);
});
test("idle persist still decides from the dossier once and does not invent collect_evidence", () => {
@@ -5,6 +5,7 @@ import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { createRectificationV9AgentTools, createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
EVIDENCE_ID,
FOCUS_ID,
@@ -212,6 +213,7 @@ test("agent tools omit offer-candidates when proposeAllowed is false", () => {
turnId: TURN_ID,
accounting: fakeAccounting({}).client as never,
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: [],
@@ -25,6 +25,7 @@ const DECISION_RECEIPT = {
display_allowed: true,
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
accept_allowed: true,
confirm_allowed: false,
@@ -53,6 +53,13 @@ export const SOURCE_TURN_ID = "77777777-7777-4777-8777-777777777777";
export const RECTIFICATION_SKILL_SHA256 = "5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d";
export const RECTIFICATION_SKILL_SOURCE_COMMIT = "0fd111d16b45796086a6c1d0945dbd3de6755d8a";
export const OPEN_ENGINE_CAPABILITY_CEILING = {
acceptanceAllowed: true,
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: true,
} as const;
export const CANDIDATE_RANGE = { start_time: "04:50", end_time: "05:10" };
export function dossierFixture(overrides: {
@@ -251,6 +258,7 @@ export function candidateSnapshotFixture(overrides: {
policy_version: "rectification-candidate-policy-v2",
selection_allowed: overrides.selectionAllowed ?? true,
acceptance_allowed: overrides.selectionAllowed ?? true,
propose_allowed: overrides.selectionAllowed ?? true,
confirmation_allowed: overrides.confirmationAllowed ?? false,
representative_candidate_id: overrides.representativeTime ? CANDIDATE_ID : null,
overall_confidence: "medium",