From 3b785b821db80430f5afda3856d6a3246ed2ae53 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 24 Aug 2026 09:40:48 +0800 Subject: [PATCH] fix(web): unblock staging web image typecheck for inference probes Docker next build rejected 909de8b8 because engine probe outcomes, reverse-verify source checks, and a nullable window scan failed TypeScript. Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 32 ++++++++++++++++ .../core/probes-from-engine.ts | 37 ++++++++++++++----- .../v9/method-followup.ts | 14 +++---- frontend/src/mastra/rectification-v9-tools.ts | 2 +- ...ctification-v10-conversation-focus.test.ts | 2 +- 5 files changed, 67 insertions(+), 20 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index bcffc29a..a89dc08b 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -5515,3 +5515,35 @@ - 复发自:BUG-362(C/D 写入后验修了,但用原地 patch 绕过缓存,receipt 不可审计) - 修复版本:待发布 +## BUG-364 | staging quality gate 在 Mac Docker runner 上 npm ci 因 bind mount 被拒 + +- 状态:investigating +- 首次发现:2026-08-24 +- 最近更新:2026-08-24 +- 影响面:Gitea `backend-quality-gate.yml` 的 `validate`「Install dependencies」步骤、`publish`(因 `needs: validate` 被 skip)、staging 发布 +- 用户现象:向 `staging` 推送后质量门失败;测试步骤未执行。`publish` skipped。 +- 触发条件:job 被调度到 `jesse-mac-xiaoxin`(Docker Desktop 里的 `ubuntu:24.04` act_runner,label `xiaoxin:host`,使用宿主机 `docker.sock`)。Linux 宿主 runner `xiaoxin` 未接走该 job。已在 run `2051`(`29750d38`)和 `2052`(`909de8b8`)复现。 +- 根因:pip 安装成功。随后 `docker run --volume "$workdir:$workdir"` 跑 `npm ci` 时,workdir 是 runner 容器内的 `/root/.cache/act//hostexecutor`。该路径不在 Docker Desktop 的 Mac 文件共享里,daemon 返回 `mounts denied`,步骤以 exit 125 失败。不是 ledger 测试或 Python 依赖错误。最近一次完整成功是 run `2038`,跑在 Linux `xiaoxin` 上。 +- 修复:未修复。候选路径是让 Linux `xiaoxin` 重新在线并接 `runs-on: xiaoxin` 的 job,或不要用套了宿主机 docker.sock 的 Mac 容器跑这个 workflow。不要靠在 Docker Desktop 里共享 Linux 容器内部路径。 +- 验证:Gitea job `5135` 日志含 `Successfully installed` 全套 pip 包,随后 `docker: Error response from daemon: mounts denied` 与 `frontend npm ci failed with status 125`。job `5133`(run `2051`)同一错误。尚未在 Linux `xiaoxin` 上重跑 `909de8b8`。 +- 防复发:`runs-on: xiaoxin` 不得由无法给 job workdir 做 bind mount 的嵌套 Docker runner 抢占。质量门失败不得在未读 Install dependencies 日志时当成业务测试失败。 +- 相关记录:BUG-136、BUG-149、BUG-266、BUG-363、BUG-365 +- 复发自:无 +- 修复版本:未修复 + +## BUG-365 | staging web 镜像 next build 被推断类型检查挡住 + +- 状态:resolved(本地修复,待提交与发布) +- 首次发现:2026-08-24 +- 最近更新:2026-08-24 +- 影响面:`deploy/railway-web.Dockerfile` 的 `RUN npm run build`、`probeFromEngine`、`method-followup` 反推探针、`rectification-v9-tools` window scan +- 用户现象:Linux `xiaoxin` 离线时在本机构建 `linux/amd64` web 镜像,`next build` 在 TypeScript 阶段失败,无法把 `909de8b8` 换上 staging。 +- 触发条件:当前 `staging` 头含推断 ledger,且走 Docker `next build`。staging push 的 validate 跳过 `npm run build`,因此测试可绿、镜像红。 +- 根因:`EngineProbeFields` 把 `expected_outcomes.answer_class` 收成 `AnswerClass`,与引擎探针的 `string` 相交后不能 `map(probeFromEngine)`。反推探针用字面量比较 `dasha_boundary`,在 `source` 收窄成 `age_band` 后被判无交集。`windowScanFromDecisionReceipt` 可为 `null`,调用方仍读 `.transitions`。 +- 修复:`probeFromEngine` 接受引擎探针并校验 `AnswerClass`。反推探针改走 `Set`。window scan 用可选链,缺省空数组。 +- 验证:本机 `npx tsc --noEmit` 仅余测试夹具多余字段,已改为 `Object.assign`;Docker `next build` 待用新 SHA 重跑。 +- 防复发:staging push 的类型回归仍只在 publish Docker 暴露;本机/xiaoxin 镜像构建失败不得改 SHA 蒙混上线。 +- 相关记录:BUG-355、BUG-363、BUG-364 +- 复发自:BUG-355(validate 跳过 `next build`,publish 才暴露类型错误) +- 修复版本:待发布 + diff --git a/frontend/src/lib/rectification-agentic/core/probes-from-engine.ts b/frontend/src/lib/rectification-agentic/core/probes-from-engine.ts index 27e5d59c..83552d80 100644 --- a/frontend/src/lib/rectification-agentic/core/probes-from-engine.ts +++ b/frontend/src/lib/rectification-agentic/core/probes-from-engine.ts @@ -1,14 +1,9 @@ import type { DiscriminatingEventProbe } from "../v9/refinement-packet.ts"; -import type { ConflictProbe, ProbeOutcome } from "./types.ts"; +import type { AnswerClass, ConflictProbe, ProbeOutcome } from "./types.ts"; -export type EngineProbeFields = DiscriminatingEventProbe & { - information_gain?: number; - semantic_key?: string; - candidate_split_hash?: string; - expected_outcomes?: readonly ProbeOutcome[]; - left_time?: string; - right_time?: string; -}; +const ANSWER_CLASSES: ReadonlySet = new Set(["yes", "weak_yes", "no", "unsure"]); + +export type EngineProbeFields = DiscriminatingEventProbe; export function probeFromEngine(probe: EngineProbeFields): ConflictProbe { const semanticKey = probe.semantic_key ?? `${probe.domain}.${probe.year}`; @@ -22,12 +17,34 @@ export function probeFromEngine(probe: EngineProbeFields): ConflictProbe { year: probe.year, question: probe.user_meaning, candidate_ids: [probe.left_time, probe.right_time].filter((item): item is string => Boolean(item)), - expected_outcomes: probe.expected_outcomes ?? defaultOutcomes(probe.left_time, probe.right_time), + expected_outcomes: outcomesFromEngine( + probe.expected_outcomes, + probe.left_time, + probe.right_time, + ), information_gain: probe.information_gain ?? 0, source: probe.source, }; } +function outcomesFromEngine( + rows: DiscriminatingEventProbe["expected_outcomes"], + left?: string, + right?: string, +): readonly ProbeOutcome[] { + if (!rows) return defaultOutcomes(left, right); + const parsed: ProbeOutcome[] = []; + for (const row of rows) { + if (!ANSWER_CLASSES.has(row.answer_class)) continue; + parsed.push({ + answer_class: row.answer_class as AnswerClass, + supports: row.supports, + conflicts: row.conflicts, + }); + } + return parsed.length > 0 ? parsed : defaultOutcomes(left, right); +} + function defaultOutcomes(left?: string, right?: string): ProbeOutcome[] { if (!left || !right || left === right) return []; return [ diff --git a/frontend/src/lib/rectification-agentic/v9/method-followup.ts b/frontend/src/lib/rectification-agentic/v9/method-followup.ts index 6c6d3182..93612bb9 100644 --- a/frontend/src/lib/rectification-agentic/v9/method-followup.ts +++ b/frontend/src/lib/rectification-agentic/v9/method-followup.ts @@ -228,6 +228,11 @@ const PROBE_METHOD_ID = { health_pressure: "d30_health", } as const; +const CONFLICT_PROBE_SOURCES = new Set([ + "dasha_boundary", + "dasha_activation", +]); + function remainingReverseVerifyProbes( probes: readonly DiscriminatingEventProbe[] | undefined, evidence: readonly MethodFollowupEvidence[], @@ -239,7 +244,7 @@ function remainingReverseVerifyProbes( if (probe.source === "known_event_quality" || probe.role === "distinguish") continue; if (declined.has(probe.domain)) continue; if (hasDatedEvidenceInYear(evidence, probe.domain, probe.year)) continue; - if (probe.source === "dasha_boundary" || probe.source === "dasha_activation" || probe.source === "dasha_boundary" || probe.source === "dasha_activation") { + if (CONFLICT_PROBE_SOURCES.has(probe.source)) { dasha.push(probe); } else { fallback.push(probe); @@ -248,13 +253,6 @@ function remainingReverseVerifyProbes( return [...dasha, ...fallback].slice(0, MAX_REVERSE_VERIFY); } -const CONFLICT_PROBE_SOURCES = new Set([ - "dasha_boundary", - "dasha_activation", - "dasha_boundary", - "dasha_activation", -]); - function remainingConflictProbes( probes: readonly DiscriminatingEventProbe[] | undefined, evidence: readonly MethodFollowupEvidence[], diff --git a/frontend/src/mastra/rectification-v9-tools.ts b/frontend/src/mastra/rectification-v9-tools.ts index da3eeef2..f5d62489 100644 --- a/frontend/src/mastra/rectification-v9-tools.ts +++ b/frontend/src/mastra/rectification-v9-tools.ts @@ -638,7 +638,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) { evidence: parsed.scorable, probes: refinement.discriminating_event_probes, previous: previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null), - transitionTimes: windowScan.transitions.map((item) => item.at), + transitionTimes: windowScan?.transitions.map((item) => item.at) ?? [], }); const persisted = await persistV9Candidate(accounting, userId, targetCaseId, { engineResultId: score.engineResultId, diff --git a/frontend/tests/rectification-v10-conversation-focus.test.ts b/frontend/tests/rectification-v10-conversation-focus.test.ts index 1ba329f1..e99071d9 100644 --- a/frontend/tests/rectification-v10-conversation-focus.test.ts +++ b/frontend/tests/rectification-v10-conversation-focus.test.ts @@ -400,7 +400,7 @@ test("resolve-focus C without new evidence appends an inference transition", asy }], }); const snapshot = candidateSnapshotFixture(); - snapshot.decision_receipt = { ...snapshot.decision_receipt, inference_state: inference }; + Object.assign(snapshot.decision_receipt, { inference_state: inference }); let appendedState: unknown = null; const accounting = fakeAccounting({ ...receiptHandlers,