Skip to content

fix: low-risk bug hunt — Tier A (11 verified fixes)#63

Open
ystyleb wants to merge 4 commits into
ExplosiveCoderflome:mainfrom
ystyleb:fix/low-risk-bug-hunt-tier-a
Open

fix: low-risk bug hunt — Tier A (11 verified fixes)#63
ystyleb wants to merge 4 commits into
ExplosiveCoderflome:mainfrom
ystyleb:fix/low-risk-bug-hunt-tier-a

Conversation

@ystyleb
Copy link
Copy Markdown
Contributor

@ystyleb ystyleb commented Jun 2, 2026

背景

对全代码库做了一次低/零风险 bug 排查:9 路 code-reviewer 并行扫描 → 每候选对抗式验证 → Codex 独立第二意见 → 关键项作者亲自读码裁决。原始 30 候选 → 去重 21 确认 → Codex 复审后分级(详见 docs/bug-hunt-low-risk-proposals.md)。

本 PR 实现 Tier A 的 11/12(A9 暂缓——产品意图待确认)。全部为机械式、意图明确、低/零风险修复。

改动

server(commit 1)

文件 修复
prompting/core/contextSelection.ts dropOrder 比较器方向反了——"应最先丢弃"的组反被优先保留。改 rightDrop - leftDrop
director/.../directorExecutionStepModules.ts Boolean(progress?.recoverableRange) 恒为真(该字段永远是非空对象)→ 改判 startOrder != null
director/.../directorWorkflowStepShared.ts 删除 resolveCurrentScopedChapter 里重复的 status==="running" 死代码。
workflow/NovelWorkflowApplicationService.ts recordCheckpoint 是唯一缺 Math.max 进度防回退的路径(其余 6 处都有)。已确认无调用方依赖"回退进度"。
planner/replanDecision.ts buildWindowOrders surrounding 兜底把重复 clamp 值计入 windowSize,anchor 靠近第 1 章时少返章节。改用 Set 去重,export 供测。
settings/ProviderBalanceService.ts response.json()await——finallyclearTimeout 在读 body 前触发,慢响应逃过 12s 超时。
task/adapters/NovelWorkflowTaskAdapter.ts 恒真三元 type===X ? X : X 把不可信 action.type 洗白成合法值;改为拒绝未知类型。

client(commit 2)

文件 修复
cover/NovelCoverDialog.tsx 删除/设为封面按钮用 mutation.variables === id 未配 isPending——删除失败后按钮永久禁用 / 卡"删除中..."。
lib/directorTaskNotice.ts 同上恒真三元(client 侧),拒绝未知 action 类型。
worlds/WorldWorkspace.tsx 两处 useWorldLibraryItem().then().catch——API 错误被静默吞掉。
creativeHub/useCreativeHubRuntime.ts loadThread().then().catch——网络错误时 UI 永久卡 loading。
writingFormula/useWritingFormulaCreateFlow.ts refreshStyleData().then().catch——刷新失败丢失自动打开回调。

docs(commit 3)

docs/bug-hunt-low-risk-proposals.md:完整排查结果 + Tier A/B/C/D 分级(含 Codex 把 buildPipelineStageProgress 等降级为"非 bug、勿改"的记录)。

测试与验证

  • TDD(RED→GREEN 实跑):contextSelectionDropOrderproviderBalanceTimeout(绑定自身 abort handle 防全局 mock 污染)、replanWindowOrdersdirectorTaskNotice.test.mjs + chapter-execution recover guard。
  • server:878 通过 / 0 失败client:36 通过 / 0 失败
  • 三端 typecheck + lint 全绿
  • Codex 复审本 PR diff:独立确认 A2/A8/A11 三个最微妙处,无 BLOCKER

暂缓 / 后续

  • A9llmStoremaxTokens 换选择时重置):可能是有意产品决策(maxTokens 模型相关),落入意图灰区,未改。
  • 未触及 Tier B(如 validate 中间件在 Express 5 下丢弃 query/params coercion,系统性)/ Tier C(非 bug)/ Tier D(Codex 新发现)。

🤖 Generated with Claude Code

ystyleb and others added 4 commits June 2, 2026 20:00
From an automated bug hunt (parallel code-reviewers + Codex second opinion),
all verified low/zero-risk:

- prompting/contextSelection: sortOptionalBlocks dropOrder comparator was
  inverted (leftDrop-rightDrop), so the group meant to be dropped FIRST was
  selected/kept first and groups absent from dropOrder were dropped first.
- director/recoverableRange: Boolean(progress?.recoverableRange) was always
  true (the field is always a non-null object); resumability now checks
  startOrder != null.
- director/resolveCurrentScopedChapter: removed a dead duplicate
  status === "running" lookup (the first `active` search already covered it).
- workflow/recordCheckpoint: the only progress update path lacking the
  Math.max(existing.progress, ...) guard could regress task progress; aligned
  with the other update sites. (Verified no caller uses checkpoints to lower
  progress — recovery/requeue use separate paths.)
- planner/buildWindowOrders: surrounding-mode fallback counted duplicate
  clamped chapter numbers against windowSize, returning fewer chapters when the
  anchor was near chapter 1. Track unique numbers with a Set; exported for test.
- settings/ProviderBalanceService: missing `await` on response.json() let the
  finally block clear the 12s abort timer before the body was read.
- task/NovelWorkflowTaskAdapter: tautology `type === X ? X : X` laundered any
  untrusted action.type into "open_structured_outline"; now rejects unknown.

Tests (TDD red→green): contextSelectionDropOrder, providerBalanceTimeout
(scoped to its own abort handle to avoid global-mock pollution),
replanWindowOrders, and chapter-execution recover guards.
Full server suite: 878 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the same low-risk bug hunt, all verified low/zero-risk:

- cover/NovelCoverDialog: the delete (and set-primary) buttons used
  deleteAssetMutation.variables === asset.id without isPending. react-query
  keeps `variables` after a mutation settles, so a FAILED delete left the
  button permanently disabled / stuck on "删除中...". Gate on isPending too.
- lib/directorTaskNotice: tautology `type === X ? X : X` accepted any untrusted
  action.type and laundered it into "open_structured_outline"; now rejects
  unknown action types (mirrors the server adapter fix).
- worlds/WorldWorkspace: onInjectLibraryField/Structure fired
  useWorldLibraryItem().then() with no .catch — API errors were silently
  swallowed. Surface via toast.error.
- creativeHub/useCreativeHubRuntime: loadThread().then() had no .catch, so a
  network error left the UI stuck on the loading state. Catch, unstick
  (setThreadStateLoaded(true)), and toast the error.
- writingFormula/useWritingFormulaCreateFlow: refreshStyleData().then() had no
  .catch, so a refresh failure dropped the auto-open callback. Still notify the
  user (the profile is already saved server-side).

Test (TDD red→green): directorTaskNotice.test.mjs (keeps valid action, rejects
unknown type). The .catch/disabled-state fixes are framework glue not covered
by the node test harness (no React render); verified by typecheck.
Full client suite: 36 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the full bug-hunt result (30 candidates -> 21 confirmed -> Codex
re-grading) and the Tier A/B/C/D classification, including:
- Tier A: the 12 low/zero-risk items (11 implemented this round).
- Tier B: real bugs needing care (e.g. validate middleware drops query/params
  coercion under Express 5 read-only req.query).
- Tier C: items Codex downgraded to non-bugs (e.g. buildPipelineStageProgress
  is correct as-is; "fixing" it would introduce a bug).
- Tier D: new findings from Codex (NovelEdit artifact_delta whitelist, etc).
- A9 (llmStore maxTokens reset) deferred pending product-intent confirmation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…a number

The validate middleware validates query schemas but does not write the coerced
result back to req.query (and Express 5's req.query is a read-only getter, so it
cannot simply be reassigned in the middleware). This route trusted
`req.query as z.infer<...>`, so chapterOrder — declared z.coerce.number() —
arrived as the raw string "5". getPayoffLedger guards on
`typeof chapterOrder === "number"`, so it silently dropped the string and
returned the default chapter's ledger instead of the requested one.

Re-parse req.query in the handler (matching the dominant pattern elsewhere,
e.g. novelBaseRoutes/rag/autoDirectorFollowUps) so the z.coerce applies. This is
the surgical, zero-blast-radius fix for the single endpoint that actually
manifests the shared-middleware flaw (Tier B "B1"); the broader middleware
hardening (write coerced query/params back under Express 5) is left as a
separate follow-up.

Verified the flaw is otherwise latent: all other coercing query routes already
re-parse, and the rest only cast string params/enums.

Test (TDD red->green): payoffLedgerQueryCoercion — service receives a number,
undefined when the param is absent. Full server suite: 880 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant