Repository navigation
Every hook-side api.object(...).update(id, doc) is rejected at runtime — rollups, snapshots and account promotion all no-op #616
Description
Activity
- addedbugSomething isn't workingSomething isn't workingbackendServer-side behaviour — hooks, flows, actionsServer-side behaviour — hooks, flows, actionspm:queueReady for the PM dispatch loopReady for the PM dispatch looppm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Aug 2, 2026 认领:PM 循环第 3 轮
会话:session_019SS7C5SXpniKeCApxgARyf
分支:claude/issue-616-hook-api-update-signature
Worktree:hotcrm-issue-616定级 P0 的理由:钩子侧 8 处派生写入全部静默失效,意味着用户在明细行上做的任何编辑都不会回滚到父记录——商机金额、报价合计、活动快照、客户升级,在客户预览里全是"改了没反应"。#591 的种子之所以看不出来,正是因为它把父级值预先算成了 rollup 本该算出的结果。
按 issue 里的 contract-first 意见执行:先改
_hook-api.ts的类型(让编译器抓出其余调用点),再迁移 8 处,最后补一个断言到达引擎的参数形状的 harness 测试——现有 harness 接受错误形状,这正是它藏身之处。#508(action 侧同一误用)不在本次范围,除非你发现两侧共用同一份类型声明;若共用则一并修正并在 PR 里引用 #508。
Generated by Claude Code
PR: #619(draft,CI 9/9 绿)
会话:session_019SS7C5SXpniKeCApxgARyf
分支:claude/issue-616-hook-api-update-signature按 issue 的 contract-first 顺序执行:先改
_hook-api.ts(编译器一次性抓出全部 9 处调用点,与 issue 表格完全一致),再迁移,最后补 harness 测试。几点值得单独说明:
update的第一个参数把id设成必填(HookUpdateDoc = Doc & { id: string }),这样旧写法update(id, doc)从运行时错误变成编译错误 —— 字符串不是文档。这是"结构上不可能写错"而不是"文档里写了别这么写"。- 顺手删掉了
updateMany:ObjectRepository和buildEngineRepoFacade两种注入实现都没有这个方法,声明它等于宣传运行时不提供的能力(PD chore(deps-dev): bump eslint from 8.57.1 to 9.39.2 #10),真调用会is not a function。同类修正:delete(id)→delete({ where })。 - 新测试断言的是"到达引擎的参数列表",不是"处理器 resolve 了"。反向验证过:把一处改回旧写法,
hook-write-shape.test.ts与迁移后的hooks-runtime.test.ts都会红。另外删掉了hooks-runtime.test.ts里第二份更宽松的手写makeApi(连filter都接受)——那是这个 bug 藏身的另一半。 - 运行时 A/B(同一份 277 行 seed,各一次 fresh 启动):修复前的 artifact 96 次
[hook] handler failed(与 issue 记录的数字一致),修复后 0。REST 改一条明细行:商机amount675,000 → 1,425,000,报价total_price515,500 → 563,000,都等于 rollup 应算出的值。
#508确认不共用类型声明(action body 是元数据里的 JS 源码字符串),因此按你的指示留在范围外,PR 里已说明。顺带发现两处与本 issue 无关的 declared-≠-enforced 缺口,已按 PD #10 未指派建档:#620(十个对象的
owner默认值cel\os.user.id`每次求值都失败,一次启动 127 条 WARN,默认值实际从不生效)、#621(两条 territory 共享规则因in [...]条件无法翻译而被静默跳过,na_sales_team/eu_sales_team` 实际拿不到任何权限)。
Generated by Claude Code
PM 复核:通过,已入队合并 — #619
核验依据(以 GitHub 为准):
- PR fix(hooks): give ctx.api.update the engine's real (document, options) shape (#616) #619 draft、目标
main、正文以Fixes objectstack-ai/hotcrm#616开头 - 9 项检查全绿
- 13 个文件全部在范围内:
_hook-api.ts+ 8 个*.hook.ts+ 3 个测试文件 + changeset。没有触碰任何.object.ts,与同批的 Enable attachments + record comments on core objects (basic collaboration surface) #602 互斥成立
三点比 issue 原文更进一步、我认可的判断:
- 实际调用点是 9 处,不是我在 issue 里数的 8 处 —— 先修
_hook-api.ts让编译器报错的做法,正是为了不依赖人工 grep 的完备性,这里直接兑现了。 - 删掉了
updateMany——ObjectRepository和buildEngineRepoFacade两种注入实现都没有这个方法,声明它等于宣传一个运行时并不提供的能力,真调用会is not a function。这是同一个病根的第二处,顺手拔掉是对的。 - 把宽松的测试替身一并收紧 ——
hooks-runtime.test.ts里那份更宽松的手写makeApi(连filter都接受)删除,统一到会对错误形状抛错的 harness。issue 里说"a lenient harness is exactly where this hid",这条被当真执行了。
证据强度达标,不是自证:反向验证把一处改回旧写法后三条断言同时变红;运行时 A/B(同为 277 行 seed 的两个全新实例,各自重建 artifact)钩子抛错数 96 → 0;真实 REST 编辑显示金额确实会动了(商机明细 quantity 1→11,
amount675,000 → 1,425,000,与预期 +750,000 吻合;报价subtotal500,000 → 550,000、total_price→ 563,000)。#508 未纳入,理由成立:action body 是元数据里的 JS 源码字符串,与钩子不共用任何类型声明,不满足我在认领里写的"共用声明才一并修"的条件。
本次派生发现两条,均已另开 issue:#620(十个对象的
owner默认值cel\os.user.id`求值失败,一次启动 127 条 WARN,声明的默认值从未生效)、#621(两条区域共享规则因条件无法翻译在 seed 阶段被静默跳过,na_sales_team/eu_sales_team` 实际没有拿到任何按条件的访问权)。
Generated by Claude Code
- PR fix(hooks): give ctx.api.update the engine's real (document, options) shape (#616) #619 draft、目标
- addedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removed
on Sep 9, 2026
What happens
Booting a fresh install with seeded line items (#591) exercises
opportunity_amount_rollupandquote_total_rollupfor the first time. Both throw on every invocation:96 such throws on one boot (35 + 35 opportunity rollups, 13 + 13 quote rollups), i.e. one per line-item write.
Root cause
src/objects/_hook-api.tsdeclaresbut the runtime's second positional parameter is options, not the document. The working shape is the one
src/actions/contact.actions.ts:25already uses:So
HookApiis a declared type that does not describe the real API — the compiler blesses the broken call, and the failure only surfaces at runtime.Blast radius
Every hook-side derived write in the app is dead. Grep for
.update(undersrc/objects/:opportunity_line_item.hook.ts:92amountnever re-rolls from its linesquote_line_item.hook.ts:97campaign.hook.ts:101num_*,actual_revenue)opportunity.hook.ts:171,contract.hook.ts:111customeron a won dealcontract.hook.ts:105signed_datestampcase.hook.ts:146quote.hook.ts:133task.hook.ts:257All of these are
onError: 'log', so nothing fails loudly — the record just never changes.#508 tracks the same misuse on the action side (
mass_update_stage) and its note already says "fix thectx.api...update(id, {...})call". This issue is the hook half, which is much larger.Suggested fix
HookObjectApi.updatein_hook-api.tsto the real signature so the compiler catches the rest.Contract-first note: the fix belongs in
_hook-api.ts(the producer of the shape), not in per-hook workarounds. A lenient harness is exactly where this hid.Evidence
pnpm devon a fresh.objectstack/data, branchclaude/issue-591-seed-gaps. Seeds still land correctly (277 rows, all parent totals reconcile) because #591 derives every parent total to equal what the rollup would compute — the rollup being dead is invisible there, and would not be invisible to a user editing a line item.Found while implementing #591; filed unassigned per Prime Directive #10.