fix(rectification): typecheck answered probe_id for staging publish
Docker next build failed on ProbeAnswer.id and optional decisionReceipt, so staging never received the month-lock SHA. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6221,6 +6221,22 @@
|
||||
- 复发自:BUG-405(排序公式对,目录被投影饿死,已打开低分卡锁题)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-409 | staging publish 的 next build 因 answered_probes.id 与 optional receipt 失败
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-28
|
||||
- 最近更新:2026-08-28
|
||||
- 影响面:Gitea `backend-quality-gate.yml` `publish`、`deploy/railway-web.Dockerfile` 的 `RUN npm run build`、`contrastPacketFromLatestResult`、Mastra `rectification-v9-tools`
|
||||
- 用户现象:向 `staging` 推送月份精度修复后,run `2128` validate 通过,publish 在 web 镜像 `next build` 失败。没有 dispatch `Deploy staging`。公网仍停在上一成功 SHA。
|
||||
- 触发条件:staging push 的 validate 跳过 `npm run build`;publish 才在 Docker 里跑 TypeScript。
|
||||
- 根因:`answered_probes` 的字段是 `probe_id`,目录过滤写成了不存在的 `item.id`,`tsc` 报 TS2339。工具侧 `latestResult.decisionReceipt` 可为 `undefined`,目录函数要求 `| null`,报 TS2345。BuildKit 日志末尾仍是 `skill-package-registry.ts` 的 Import traces,真正失败是 `Failed to type check`。
|
||||
- 修复:已答探针按 `probe_id` 过滤。目录函数接受 `undefined` latest,并把缺省 receipt 收成 `null` 再往下传。
|
||||
- 验证:`npx tsc --noEmit`;`rectification-decision-authority` 锁定已答探针按 `probe_id` 退出目录。
|
||||
- 防复发:改生时纠正目录或 Mastra 工具类型后,推 staging 前必须跑 `npx tsc --noEmit` 或等价 `next build`,不能只跑 `tsx --test`。
|
||||
- 相关记录:BUG-309、BUG-315、BUG-321、BUG-371、BUG-408
|
||||
- 复发自:BUG-309(validate 跳过 `next build`,publish 才暴露类型错误)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-408 | 反推题丢掉大运起点的月份,同年 3 月对 9 月问不出来
|
||||
|
||||
- 状态:resolved
|
||||
|
||||
@@ -60,7 +60,7 @@ export type DecisionDossier = Readonly<{
|
||||
};
|
||||
latestResult: {
|
||||
resultId?: string;
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null;
|
||||
decisionReceipt?: Readonly<Record<string, unknown>> | null;
|
||||
selectionAllowed?: boolean;
|
||||
confirmationAllowed?: boolean;
|
||||
candidates?: readonly Readonly<{
|
||||
@@ -124,13 +124,13 @@ function holdoutStatusFromState(state: InferenceState) {
|
||||
}
|
||||
|
||||
export function contrastPacketFromLatestResult(
|
||||
latest: DecisionDossier["latestResult"],
|
||||
latest: DecisionDossier["latestResult"] | undefined,
|
||||
evidence: DecisionDossier["evidence"] = [],
|
||||
): CandidateContrastPacket {
|
||||
const windowScan = windowScanFromDecisionReceipt(latest?.decisionReceipt ?? null);
|
||||
const refinement = refinementFromDecisionReceipt(latest?.decisionReceipt ?? null);
|
||||
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
|
||||
const answered = new Set((inference?.answered_probes ?? []).map((item) => item.id));
|
||||
const answered = new Set((inference?.answered_probes ?? []).map((item) => item.probe_id));
|
||||
const fromInference: EngineContrastProbe[] = (inference?.probes ?? []).flatMap((probe) => {
|
||||
if (answered.has(probe.id) || probe.information_gain <= 0) return [];
|
||||
if (probe.source === "known_event_quality") return [];
|
||||
@@ -172,7 +172,7 @@ export function contrastPacketFromLatestResult(
|
||||
? [{ layer: "d10", signs: windowScan.d10_sign_names }]
|
||||
: []),
|
||||
],
|
||||
candidateTimes: discriminatorCandidateTimes(latest),
|
||||
candidateTimes: discriminatorCandidateTimes(latest ?? null),
|
||||
transitions: windowScan?.transitions ?? [],
|
||||
askedKeys: askedDiscriminatorKeys(latest?.decisionReceipt, evidence),
|
||||
volunteeredDomains: volunteeredDomainsFromEvidence(evidence),
|
||||
@@ -202,15 +202,15 @@ export function contrastPacketFromDossier(dossier: DecisionDossier): CandidateCo
|
||||
}
|
||||
|
||||
export function rectificationFollowupCatalog(
|
||||
latest: DecisionDossier["latestResult"],
|
||||
latest: DecisionDossier["latestResult"] | undefined,
|
||||
evidence: DecisionDossier["evidence"] = [],
|
||||
) {
|
||||
const receipt = latest?.decisionReceipt ?? null;
|
||||
const refinement = refinementFromDecisionReceipt(receipt);
|
||||
const inference = previousInferenceFromReceipt(receipt);
|
||||
return {
|
||||
contrastPacket: contrastPacketFromLatestResult(latest, evidence),
|
||||
topCandidateTimes: discriminatorCandidateTimes(latest),
|
||||
contrastPacket: contrastPacketFromLatestResult(latest ?? null, evidence),
|
||||
topCandidateTimes: discriminatorCandidateTimes(latest ?? null),
|
||||
askedProbeKeys: askedDiscriminatorKeys(receipt, evidence),
|
||||
eventProbes: refinement.discriminating_event_probes,
|
||||
eventClarificationProbes: refinement.event_clarification_probes,
|
||||
|
||||
@@ -189,7 +189,7 @@ function contrastPacketFromLatest(
|
||||
latest: NonNullable<DossierForTools["latestResult"]> | null | undefined,
|
||||
evidence: DossierForTools["evidence"] = [],
|
||||
) {
|
||||
return contrastPacketFromLatestResult(latest, evidence);
|
||||
return contrastPacketFromLatestResult(latest ?? null, evidence);
|
||||
}
|
||||
|
||||
function snapshotSourceFromDossier(
|
||||
|
||||
@@ -146,6 +146,70 @@ test("recorded education evidence does not suppress an unasked D24 discriminator
|
||||
assert.equal(packet.probes[0]?.expectedOutcomes.at(-1)?.outcomeId, "unsure");
|
||||
});
|
||||
|
||||
test("answered inference probes drop out of the catalog by probe_id", () => {
|
||||
const outcomes = [
|
||||
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:12"] },
|
||||
{ answer_class: "no" as const, supports: ["05:12"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
|
||||
];
|
||||
const packet = contrastPacketFromDossier({
|
||||
evidence: [],
|
||||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||||
latestResult: {
|
||||
resultId: "result-answered-probe",
|
||||
candidates: [
|
||||
{ candidateId: "c-0500", time: "05:00", relativeSupport: 20 },
|
||||
{ candidateId: "c-0512", time: "05:12", relativeSupport: 15 },
|
||||
],
|
||||
decisionReceipt: {
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: "05:00-05:12:05:00,05:12",
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "discriminating",
|
||||
range_start: "05:00",
|
||||
range_end: "05:12",
|
||||
candidates: [
|
||||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 20, posterior_score: 20, probability: 0.6, status: "active", rank: 1, strong_conflict_count: 0 },
|
||||
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:12"], prior_score: 15, posterior_score: 15, probability: 0.4, status: "active", rank: 2, strong_conflict_count: 0 },
|
||||
],
|
||||
events: [],
|
||||
probes: [{
|
||||
id: "probe:career.2018.dasha_boundary:hash",
|
||||
year: 2018,
|
||||
domain: "career",
|
||||
source: "dasha_boundary",
|
||||
question: "2018 年 3 月前后 career",
|
||||
semantic_key: "career.2018.dasha_boundary",
|
||||
candidate_ids: ["05:00", "05:12"],
|
||||
information_gain: 1.2,
|
||||
expected_outcomes: outcomes,
|
||||
candidate_split_hash: "hash",
|
||||
}],
|
||||
answered_probes: [{
|
||||
probe_id: "probe:career.2018.dasha_boundary:hash",
|
||||
semantic_key: "career.2018.dasha_boundary",
|
||||
candidate_split_hash: "hash",
|
||||
answer_class: "yes",
|
||||
classified_from: "choice",
|
||||
}],
|
||||
rounds: [],
|
||||
entropy: 1,
|
||||
representative_time: "05:00",
|
||||
credible_range: ["05:00", "05:12"],
|
||||
},
|
||||
},
|
||||
},
|
||||
case: { acceptedTime: null },
|
||||
});
|
||||
assert.equal(packet.probes.some((probe) => probe.semanticKey === "career.2018.dasha_boundary"), false);
|
||||
|
||||
const adapter = readSource("../src/lib/rectification-agentic/v9/decision-from-dossier.ts");
|
||||
assert.match(adapter, /answered_probes[\s\S]{0,120}item\.probe_id/);
|
||||
assert.doesNotMatch(adapter, /answered_probes[\s\S]{0,120}item\.id\)/);
|
||||
});
|
||||
|
||||
test("scored inference catalog outranks a low-gain Python career probe when snapshot candidates are empty", () => {
|
||||
const careerOutcomes = [
|
||||
{ answer_class: "yes", supports: ["04:45", "05:00", "05:14"], conflicts: ["05:15"] },
|
||||
|
||||
Reference in New Issue
Block a user