feat(runtime): action body 被丢弃的 ctx.record 写在调用时可见 (#4345) - #4357
Merged
Conversation
…ted, not silently discarded (#4345) `ctx.record` in an L2 action body is a pre-fetched snapshot that nothing writes back, so every assignment to it was dropped — a correctly spelled, fully declared field exactly like an unknown one — while the action returned success. No diagnostic anywhere: the #4001 "silent no-op manufactures false completion" shape, one layer below the unknown-column drop #4271 covers. The snapshot stays read-only. An action's output is its return value and its write channel is `ctx.api`; making the snapshot writable would raise questions this bug does not answer (write back to what, under whose permissions, what `requiresRecord: false` means). What was wrong was the silence. Runtime: the sandbox installs a set/deleteProperty/defineProperty proxy over the snapshot, behind an accessor so a wholesale `ctx.record = {…}` cannot swap the recorder out, and reports the touched keys as `ScriptResult.droppedRecordWrites`. `actionBodyRunnerFactory` warns naming the discarded fields and the `ctx.api.object(...).update(...)` remedy. Writes still work inside the VM, so a body using the snapshot as scratch keeps its reads coherent — only the silence is removed. As a run-time trap it sees the computed keys, `Object.assign` and aliases static analysis cannot, and it covers metadata authored through Studio or the API, which no lint inspects. Hooks carry no `record` and install no proxy. Lint: new advisory rule `action-body-record-write-discarded` (`validateActionRecordWrites`) in REFERENCE_INTEGRITY_RULES, so `os validate`, `os lint` and `os compile` all report it. It never consults declared fields and offers no did-you-mean — the field name is not the bug — and it dedupes defineStack's merged action copies by value, not identity, so one authored action reports once. Advisory because a body may use the snapshot as local scratch, which no analysis short of data-flow tells from an intended persist. Docs: `ScriptContext.record`, `ActionSchema.body`, `ScriptBodySchema`, `content/docs/ui/actions.mdx` and `content/docs/automation/hook-bodies.mdx` now state the read-only semantics and name the `ctx.input` analogy as the trap it is. Verified on the showcase app: zero findings before, exactly one after planting the issue's repro — naming both declared fields and proposing the very `ctx.api` call it replaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Both landed on the same surface while this branch was open, so the merge is a reconciliation rather than a textual resolution: - #4344 built the same `actions` + `objects[].actions` walk, with the same value-dedupe of defineStack's merged copies (it found the double-report the same way). Two walks over one surface would drift silently, so its `collectActionBodies` is now exported (module-level, not via the barrel — same posture as BODY_WRITE_SYSTEM_FIELDS), carries the bound object, and this rule consumes it. The duplicate walk and its helpers are deleted. - The two rules split one write surface and answer different questions, so the suite, the barrel and the spec JSDoc now say which is which: `ctx.api` writes → does this field exist (did-you-mean); `ctx.record` writes → none of them land, declared or not (no did-you-mean, because the name is not the bug). - #4351/#4355 rewrote the hook-bodies write-set section this branch also edited. Its write-shape table listed three shapes and said nothing about `ctx.record`, which this branch now checks — added the row plus the note that it is a different defect, and that the record rule's statically-opaque cases are covered by the runtime recorder instead. Verified after the merge: lint 642, spec 7160, runtime 989, cli 628, spec check:generated 8/8, typecheck + eslint clean, showcase 0 findings clean and exactly 1 with the repro planted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
#4362(已合并)为 #4345 落了 lint 规则 `action-record-write-discarded`,而且 在一处比本分支原来的规则正确:它要求写是**可证明的死写**(`ctx.record` 不逃逸)。 因为这是活的、确实落库的合法写法: ctx.record.stage = 'won'; await ctx.api.object('crm_deal').update(ctx.record); 本分支原来的 lint 规则会把它误报成"被丢弃",而且断言"stored record is unchanged" —— 那是一句错话,不只是噪音。实测确认后: - 删掉本分支的 `validate-action-record-writes.{ts,test.ts}` 及其全部接线, lint 包与 hook-bodies.mdx 整体采用 main 的版本; - **同一个缺陷也在本分支的运行时层**:它对上面那段一样报 dropped。补了逃逸 检测(写之后出现 `ownKeys` 即视为逃逸,proxy 上观察),语义与 #4362 对齐: 整体消费(交给 ctx.api、展开、返回、JSON.stringify)噤声,属性读不算解救, 所以 showcase 的 `ctx.recordId || (ctx.record && ctx.record.id)` 惯用法 照报。 保留的只剩 #4362 明确留作 open question、且 lint 够不着的那一层:运行时在 调用时报告被丢弃的写 —— 覆盖 computed key、别名、整体替换,以及 Studio / API 写入的 metadata(lint 永远看不到)。它只报告,不 refuse 也不 honour,不预设 那个 open question 的答案。 changeset 相应收窄为 @objectstack/runtime 单包。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…x-record-dropped-75463d
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4345 的运行时一半。
为什么本分支原来的 lint 规则被删掉
#4362 要求写是可证明的死写(
ctx.record不逃逸)。因为这是活的、确实落库的合法写法:本分支原来的规则会把它报成"被丢弃",并断言
the stored record is unchanged——那是一句错话,不只是噪音。实测确认后整条删除,lint 包与
hook-bodies.mdx整体采用 main 的版本。
同一个缺陷也在本分支的运行时层,一并修了(见下)。
留下的是什么:调用时报告
lint 只看字面模式、且只看经过 CLI 的 metadata。运行时能覆盖它够不着的三类:
ctx.record[k] = v、const r = ctx.record; r.x = 1,lint 刻意跳过而不猜;ctx.record = {…};os validate。沙箱在快照上装
set/deleteProperty/definePropertyproxy,放在 accessor 后面,所以整体替换也抓得到、不会把记录器换掉。命中的 key 走
ScriptResult.droppedRecordWrites,actionBodyRunnerFactory报 warn,点名字段和ctx.api.object(...).update(...)的改法。写照常在 VM 内生效,拿快照当草稿纸的 body 读写仍自洽 —— 去掉的只是沉默。
逃逸检测(与 #4362 语义对齐)
写之后出现
ownKeys即视为逃逸 —— 整体消费(交给ctx.api、展开、返回、JSON.stringify)会枚举键,而属性读不会。所以:ctx.record.stage='won'; return null;var id = ctx.recordId || (ctx.record && ctx.record.id); ctx.record.stage='won';Object.keys(ctx.record)发生在写之前ctx.record.stage='won'; await ctx.api.object('d').update(ctx.record);JSON.stringify方向和 #4362 一致:歧义算活的。一句错的"被丢弃"是在对存储状态说假话,比漏报更糟。
hook 的 ctx 没有
record,不装 proxy、零成本。ctx.record仍是只读;运行时到底该refuse 还是 honour 仍未决 —— 只报告不预设任何一边的答案。
净改动
8 个文件,只碰 runtime + 一段
ActionSchema.bodyJSDoc +actions.mdx(#4362 没动这个文件)。
验证(合流后全部重跑)
@objectstack/runtime@objectstack/lint@objectstack/spec@objectstack/clispec check:generated🤖 Generated with Claude Code