fix(rectification): stream persisted candidate choices
This commit is contained in:
@@ -2054,3 +2054,17 @@
|
||||
- 验证:TypeScript 通过;聚焦测试 90/90;完整测试 1221/1221;lint 0 error、3 个既有 warning;production build 通过;本地 PostgreSQL 验证 `04:55` 写为 `accepted`、保留 `05:00` reported time、重复采纳幂等,并验证出生申报时间变化会使结果失效且拒绝再次采纳。
|
||||
- 相关记录:BUG-113、BUG-114、BUG-115、BUG-116
|
||||
- 修复版本:待提交与发布
|
||||
|
||||
## BUG-118 | 候选已落库但确认卡不显示,VedAstro 未执行被误述为未通过
|
||||
|
||||
- 状态:resolved(local,pending deployment)
|
||||
- 首次发现:2026-08-04
|
||||
- 最近更新:2026-08-04
|
||||
- 影响面:Agentic 生时校正候选 SSE、self-hosted PostgreSQL 查询兼容层、确认门公开语义
|
||||
- 用户现象:`rectification-confirm` 已返回并持久化 `selection_allowed=true` 的候选时间,但页面只显示 Agent 文本,不显示候选确认卡;Agent 同时把 VedAstro 未执行、邻近分钟诊断和留一事件诊断混写成确认门未通过。
|
||||
- 根因:候选恢复查询调用 `.gt("expires_at", now)`,而 staging 使用的 `LocalPostgresQueryBuilder` 未实现 `gt`,路由捕获读取异常后仍发送完成事件;外部验证本身只有在本地候选满足事件数、领域数、窄区间、唯一领先和必需层完整时才执行,`missing_mandatory_layers` 会使其保持 `not_evaluated`,并非 VedAstro 调用失败。邻近分钟与留一事件在 technique contract 中仅为诊断项。
|
||||
- 修复:在共享本地 PostgreSQL query builder 中实现参数化 `gt` 过滤;保留现有候选卡与 SSE 协议不另起状态;确认工具显式返回外部验证是否已调用、状态和原因,并要求 Agent 区分 `not_evaluated` 与 `fail`,不得把诊断项描述为硬阻塞。
|
||||
- 验证:真实 local PostgreSQL business client 回归覆盖未过期候选读取;Agentic 工具回归覆盖 `not_evaluated` 映射为 `external_validation_invoked=false`;Session、entry、candidate persistence 聚焦测试通过。
|
||||
- 防复发:self-hosted query builder 新增 Supabase/PostgREST 链式操作时必须由真实 PostgreSQL fixture 覆盖;公开文案必须按 `external_engines.status` 区分未执行、失败和通过。
|
||||
- 相关记录:BUG-116、BUG-117
|
||||
- 修复版本:待本次 staging 修复提交与部署验收
|
||||
|
||||
@@ -13,6 +13,7 @@ type QueryResult = Readonly<{
|
||||
type Filter =
|
||||
| Readonly<{ kind: "eq"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "neq"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "gt"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "in"; column: string; value: readonly unknown[] }>
|
||||
| Readonly<{ kind: "is"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "notContains"; column: string; value: unknown }>;
|
||||
@@ -176,6 +177,12 @@ class LocalPostgresQueryBuilder implements PromiseLike<QueryResult> {
|
||||
return this;
|
||||
}
|
||||
|
||||
gt(column: string, value: unknown) {
|
||||
identifier(column);
|
||||
this.filters.push({ kind: "gt", column, value });
|
||||
return this;
|
||||
}
|
||||
|
||||
in(column: string, value: readonly unknown[]) {
|
||||
identifier(column);
|
||||
this.filters.push({ kind: "in", column, value });
|
||||
@@ -257,7 +264,7 @@ class LocalPostgresQueryBuilder implements PromiseLike<QueryResult> {
|
||||
return `not (${column} @> $${parameters.length})`;
|
||||
}
|
||||
parameters.push(databaseValue(types.get(filter.column), filter.value));
|
||||
return `${column} ${filter.kind === "neq" ? "<>" : "="} $${parameters.length}`;
|
||||
return `${column} ${filter.kind === "neq" ? "<>" : filter.kind === "gt" ? ">" : "="} $${parameters.length}`;
|
||||
});
|
||||
return ` where ${parts.join(" and ")}`;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ TRUTH BOUNDARIES (from the skill overlay)
|
||||
- Keep three states distinct: candidate is an engine comparison result; accepted is the user's chosen working birth time; confirmed is a unique minute that passed the engine confirmation gate and was accepted by the user.
|
||||
- You may show only the server-returned candidate times and relative_support values. Call them “相对支持度”, never probability, statistical confidence, or certainty. Never expose raw scores, weights, event ids, payloads, or chain-of-thought.
|
||||
- If confirmation_allowed=false but selection_allowed=true, explain that the engine has not uniquely confirmed one minute and let the user choose among the returned candidates. Never call that choice engine-confirmed.
|
||||
- Read external_validation_status literally: not_evaluated means official VedAstro was not invoked because its local entry gate was not ready, not that VedAstro ran and failed. Neighbor stability and leave-one-event-out are diagnostic confidence indicators, not hard blockers.
|
||||
- If confirmation_allowed=true, still require explicit user agreement before saving the representative minute.
|
||||
|
||||
SAVING
|
||||
|
||||
@@ -531,7 +531,7 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext
|
||||
const confirmTool = createTool({
|
||||
id: "rectification-confirm",
|
||||
description:
|
||||
"Run the high-rigor confirmation gate for the candidate range and the user's dated events: three-engine parity, external VedAstro validation, neighbor stability, leave-one-out retention, width and margin thresholds. Returns whether a precise minute can be confirmed, the representative minute, and the reasons. Only call once you have enough confirmed dated events across domains. This does NOT write anything.",
|
||||
"Run the high-rigor confirmation gate for the candidate range and the user's dated events. Official VedAstro runs only after the local external-validation entry gate is ready; external_validation_status=not_evaluated means it was not invoked, not that it failed. Neighbor stability and leave-one-out are diagnostic confidence indicators, not hard blockers. Returns whether a precise minute can be confirmed, the representative minute, and the reasons. Only call once you have enough confirmed dated events across domains. This does NOT write anything.",
|
||||
inputSchema: z.object({
|
||||
candidate_range: candidateRangeSchema,
|
||||
events: z.array(agenticRectificationEventSchema).min(1).max(40),
|
||||
@@ -558,6 +558,15 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext
|
||||
const gates = (technique?.gates && typeof technique.gates === "object")
|
||||
? technique.gates as Record<string, unknown>
|
||||
: {};
|
||||
const externalEngines = (technique?.external_engines && typeof technique.external_engines === "object")
|
||||
? technique.external_engines as Record<string, unknown>
|
||||
: {};
|
||||
const externalValidation = (externalEngines.validation && typeof externalEngines.validation === "object")
|
||||
? externalEngines.validation as Record<string, unknown>
|
||||
: {};
|
||||
const externalValidationStatus = typeof externalEngines.status === "string"
|
||||
? externalEngines.status
|
||||
: "not_evaluated";
|
||||
const confirmationAllowed = technique?.confirmation_allowed === true
|
||||
&& technique?.decision === "confirm_minute";
|
||||
const representativeTime = winning && timePattern.test(String(winning.representative_time ?? ""))
|
||||
@@ -604,6 +613,11 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext
|
||||
} : null,
|
||||
reasons: Array.isArray(data.reasons) ? data.reasons : [],
|
||||
stability_diagnostics: data.stability_diagnostics,
|
||||
external_validation_status: externalValidationStatus,
|
||||
external_validation_invoked: externalValidationStatus !== "not_evaluated",
|
||||
external_validation_reason: typeof externalValidation.reason === "string"
|
||||
? externalValidation.reason
|
||||
: typeof externalValidation.vedastro_reason === "string" ? externalValidation.vedastro_reason : null,
|
||||
technique_contract: {
|
||||
decision: technique?.decision,
|
||||
confirmation_allowed: technique?.confirmation_allowed,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
import { createLocalPostgresDataClient } from "../src/lib/db/local-postgres-client-core.ts";
|
||||
import { loadLatestAgenticRectificationResult } from "../src/lib/rectification-agentic/session.ts";
|
||||
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
|
||||
|
||||
const runnerPath = fileURLToPath(
|
||||
@@ -216,11 +217,22 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
|
||||
) values (
|
||||
'33333333-3333-4333-8333-333333333333', '${userId}', '${rectificationSessionId}',
|
||||
'engine-result-1', 'canonical-hash-1', 'test-v1', '{}',
|
||||
'[{"time":"04:55","relative_support":60},{"time":"05:07","relative_support":40}]',
|
||||
'[{"rank":1,"time":"04:55","relative_support":60,"tied_minute_count":1},{"rank":2,"time":"05:07","relative_support":40,"tied_minute_count":1}]',
|
||||
'medium', true, false, '04:55', '1997-08-08', '05:00', 'family_exact', 10, 10,
|
||||
36.420487, 114.209936, 8
|
||||
);
|
||||
`);
|
||||
const latestCandidate = await loadLatestAgenticRectificationResult(
|
||||
admin as never,
|
||||
userId,
|
||||
rectificationSessionId,
|
||||
);
|
||||
assert.equal(latestCandidate?.resultId, "33333333-3333-4333-8333-333333333333");
|
||||
assert.equal(latestCandidate?.selectionAllowed, true);
|
||||
assert.deepEqual(latestCandidate?.candidates.map(({ time, relative_support }) => ({ time, relative_support })), [
|
||||
{ time: "04:55", relative_support: 60 },
|
||||
{ time: "05:07", relative_support: 40 },
|
||||
]);
|
||||
assert.equal(
|
||||
fixture.psqlAs(
|
||||
"admin_runtime",
|
||||
|
||||
@@ -98,7 +98,11 @@ const confirmedEngineResponse = () => ({
|
||||
domain_count: 3,
|
||||
can_apply: true,
|
||||
winning_segment: { start_time: "14:28", end_time: "14:32", representative_time: "14:30", width_minutes: 4 },
|
||||
technique_contract: { confirmation_allowed: true, decision: "confirm_minute" },
|
||||
technique_contract: {
|
||||
confirmation_allowed: true,
|
||||
decision: "confirm_minute",
|
||||
external_engines: { status: "pass", validation: { reason: "validated" } },
|
||||
},
|
||||
reasons: [],
|
||||
missing_layers: [],
|
||||
candidate_ranking_summary: [
|
||||
@@ -375,6 +379,42 @@ test("confirm persists ranked candidates with relative support totaling 100", as
|
||||
engine.restore();
|
||||
});
|
||||
|
||||
test("confirm distinguishes skipped external validation from a failed VedAstro run", async () => {
|
||||
const engine = installEngine([{
|
||||
path: "/api/active_rectification_events",
|
||||
respond: () => {
|
||||
const response = confirmedEngineResponse();
|
||||
return {
|
||||
...response,
|
||||
body: {
|
||||
...response.body,
|
||||
can_apply: false,
|
||||
technique_contract: {
|
||||
confirmation_allowed: false,
|
||||
decision: "continue_rectification",
|
||||
external_engines: {
|
||||
status: "not_evaluated",
|
||||
validation: { reason: "local_candidate_not_ready_for_external_validation" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
}]);
|
||||
const tools = createAgenticRectificationTools(makeCtx());
|
||||
|
||||
const result = await runTool(tools, "rectification-confirm", {
|
||||
candidate_range: { start_time: "14:00", end_time: "15:00" },
|
||||
events: sampleEvents,
|
||||
});
|
||||
|
||||
assert.equal(result.selection_allowed, true);
|
||||
assert.equal(result.external_validation_status, "not_evaluated");
|
||||
assert.equal(result.external_validation_invoked, false);
|
||||
assert.equal(result.external_validation_reason, "local_candidate_not_ready_for_external_validation");
|
||||
engine.restore();
|
||||
});
|
||||
|
||||
test("accept candidate tool delegates the exact persisted candidate and preserves accepted status", async () => {
|
||||
const calls: Array<{ time: string; resultId?: string }> = [];
|
||||
const tools = createAgenticRectificationTools(makeCtx(async (time, resultId) => {
|
||||
|
||||
Reference in New Issue
Block a user