Merge pull request #85 from jesse-ux/codex/rectification-candidate-card-fix

fix(rectification): stream persisted candidate choices
This commit is contained in:
jesse-ux
2026-08-04 14:43:24 +08:00
committed by GitHub
6 changed files with 92 additions and 4 deletions
+14
View File
@@ -2054,3 +2054,17 @@
- 验证:TypeScript 通过;聚焦测试 90/90;完整测试 1221/1221lint 0 error、3 个既有 warningproduction build 通过;本地 PostgreSQL 验证 `04:55` 写为 `accepted`、保留 `05:00` reported time、重复采纳幂等,并验证出生申报时间变化会使结果失效且拒绝再次采纳。
- 相关记录:BUG-113、BUG-114、BUG-115、BUG-116
- 修复版本:待提交与发布
## BUG-118 | 候选已落库但确认卡不显示,VedAstro 未执行被误述为未通过
- 状态:resolvedlocalpending 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
+15 -1
View File
@@ -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,
+13 -1
View File
@@ -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) => {