Skip to content

feat(automation): update_record/create_record 步骤对被静默剥离的写入字段挂 warning(#3407) - #3413

Merged
os-zhuang merged 3 commits into
mainfrom
claude/update-record-silent-field-strip-l77rag
Jul 24, 2026
Merged

feat(automation): update_record/create_record 步骤对被静默剥离的写入字段挂 warning(#3407)#3413
os-zhuang merged 3 commits into
mainfrom
claude/update-record-silent-field-strip-l77rag

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#3407(从 #3356 拆出的可观测性缺口)。

问题

update_record 节点无条件返回 success,即使请求写入的字段被数据层合法丢弃(静态 readonly#2948 / 条件 readonlyWhen#3042)。数据层的 warn 只落在服务端 logger,不进流程运行的步骤日志 —— 作者在 run trace 里只看到一条 3ms 的 success,这正是 #3356 里审批流 stage 回写整链失效被掩盖的原因。

方案拍板

采用 issue 中的**方向 1(引擎结构化回传)**的回调变体,未选方向 3(日志路由):日志路由需要把 per-run logger 一路透传进引擎与剥离函数,侵入更深,且节点只能拿到非结构化文本、拿不到字段名列表。

调研中确认的两个范围收窄:

  • FLS 写门已非静默:plugin-securitydetectForbiddenWrites 现在直接抛 PermissionDeniedError(fail-closed),会表现为 success: false,不属于本单的静默剥离;
  • insert 不剥离 readonly(INSERT 豁免是既有语义,FLS 抛错),所以 create_record 今天不会产生 warning —— 但按 issue 要求对称接线,未来 insert 若新增静默剥离会自动浮现,不会重新变哑。

实现

spec(契约,@objectstack/spec)

  • data/data-engine.zod.ts:新增 DroppedFieldsEventSchema —— { object, fields, reason: 'readonly' | 'readonly_when' },Zod-first、可发布进 json-schema manifest(已随构建更新);
  • contracts/data-engine.ts:新增 WriteObservabilityOptions(onFieldsDropped 监听器),以交叉类型挂在 IDataEngine.insert/update 的 options 形参上。刻意不进可序列化的 Zod options schema:函数在 JSON Schema 不可表示(会触发 manifest ratchet 失败),也不可能跨 RPC(Virtual Data Engine)边界 —— 这是进程内通道,与 IDataEngine 本身同层(TS 契约层);
  • packages/core 的镜像契约同步(该文件当前无引用方,仅保持一致)。

objectql(engine.update())

  • 在全部 4 个剥离点(单 id / bulk × readonly / readonlyWhen)之后,对比剥离前后键集并回调 onFieldsDropped。对比是精确的:剥离函数在无丢弃时返回同一引用,有丢弃时返回浅拷贝;
  • 监听器抛错不影响写入(catch + warn);
  • system 上下文跳过 readonly 剥离、因此不产生事件 —— 行为与之前一致;
  • insert() 出于签名对称接受该 option,但今天不触发(见上)。

service-automation

  • NodeExecutionResult / StepLogEntry 新增 advisory 的 warnings?: string[],executeNode 在成功/失败步骤条目上透传,随 run history 持久化(compactStepLogForHistory 展开透传,无需改动);
  • update_record / create_record:每个剥离事件生成一条 warning(点名字段与原因,readonlyWhen 措辞覆盖 bulk 的「≥1 matched row」语义),并在 output 暴露结构化的 droppedFields(下游节点可读 {<nodeId>.droppedFields});
  • success 语义不变 —— 剥离依旧是合法语义,只是不再沉默。

测试

  • packages/objectql/src/engine.test.ts 新增 6 例:单 id readonly / 单 id readonlyWhen / bulk readonlyWhen / system 豁免不报 / 无丢弃不报 / 监听器抛错不破坏写入;
  • packages/services/service-automation/src/builtin/crud-dropped-fields.test.ts 新增 5 例:步骤 warning(成功状态保持)、droppedFields 输出变量、无剥离无 warning、create_record 对称接线;
  • 全量回归:spec 6818 ✓ / objectql 1042 ✓ / service-automation 347 ✓ / core 386 ✓;json-schema.manifest.jsonapi-surface.json 已随构建脚本再生(纯增量)。

含 changeset(spec / objectql / service-automation 各 minor)与 content/docs/kernel/contracts/data-engine.mdx 的契约文档更新。

🤖 Generated with Claude Code

https://claude.ai/code/session_01HaofCZbsPTHE2oJedvKd77


Generated by Claude Code

…ings (#3407)
update_record reported an unconditional success even when the data layer
legally stripped the requested write fields (static readonly #2948,
conditional readonlyWhen #3042) — the only trace was a server-side logger
warn, invisible in the flow run trace, which is how #3356's approval stage
write-backs failed behind a clean 3ms success.
- spec: new DroppedFieldsEventSchema ({object, fields, reason}) in
data/data-engine.zod.ts (Zod-first, JSON-representable, published to the
schema manifest) + a WriteObservabilityOptions mixin (onFieldsDropped
listener) on IDataEngine.insert/update option params in
contracts/data-engine.ts. The listener is TS-contract-level and
in-process only — a function is unrepresentable in JSON Schema and never
crosses the RPC boundary. Core's mirror contract kept in sync.
- objectql: engine.update() reports each strip pass's dropped keys +
reason through options.onFieldsDropped at all four strip sites
(single-id + bulk x readonly + readonly_when), diffing before/after key
sets (exact: strips return the same reference when nothing dropped). A
throwing listener never breaks the write. System-context writes skip the
readonly strip and report nothing, as before. insert() accepts the
option for signature symmetry but strips nothing today (INSERT is
readonly-exempt; FLS write denial throws).
- service-automation: NodeExecutionResult and StepLogEntry gain advisory
warnings?: string[] (persisted through run history; success semantics
unchanged). update_record / create_record attach one warning per strip
event naming the dropped fields and expose a structured droppedFields
output ({<nodeId>.droppedFields}) for downstream nodes.
Design per issue #3407 direction 1 (engine structured feedback) — chosen
over run-scoped logger routing (direction 3): shallower intrusion, and the
node gets a structured field list instead of log text. FLS is not wired:
its write gate now throws (fail-closed) instead of stripping, so it is no
longer a silent-drop case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaofCZbsPTHE2oJedvKd77
@vercel

vercelBot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 24, 2026 11:25am

Request Review

Three rerun_failed_jobs rounds on the runs queued during the 2026-07-22
GitHub-hosted-runner outage all failed in seconds with no logs (HTTP 404),
including after GitHub resolved the incident at 22:09 UTC. Fresh push to
mint fresh workflow runs instead of re-running the poisoned run records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaofCZbsPTHE2oJedvKd77
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

CI 红灯是仓库级 Actions 故障,与本 PR 代码无关 — 诊断如下:

  • 本仓所有分支的所有 workflow run(含其他 PR 分支 claude/field-value-shape-contract-1z4pilclaude/inverse-drift-undeclared-props-5qdonq)从 20:59 UTC 起全部在 3-4 秒内 failure,job 日志 HTTP 404(runner 从未真正启动),check run output 为空;
  • 时间窗与 GitHub 官方事故吻合(githubstatus:GitHub-hosted runners 降级,20:47 确认、22:09 宣布解决),但 22:11 UTC 空提交触发的全新 run 依旧秒死 — 属残余效应或组织级问题(若持续,建议查一下 org 的 Actions 计费/spending limit);
  • 已重踢 4 轮(3 次 rerun_failed_jobs + 1 次空提交新 run)均无效,停止无效重试。

本地全量测试是绿的(spec 6818 / objectql 1042 / service-automation 347 / core 386)。我会继续定时自检,GitHub 恢复后自动重踢并汇报。


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec.

116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via @objectstack/core)
  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/knowledge-rag.mdx(via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx(via @objectstack/core)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/core, packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/core, packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/core, @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/core)
  • content/docs/kernel/runtime-services/index.mdx(via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authorization.mdx(via packages/core, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx(via @objectstack/core)
  • content/docs/plugins/development.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v15.mdx(via @objectstack/core)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

CI 全绿,已转 ready for review。

GitHub Actions 恢复(7/24 ~11:20 UTC)后:

  • 恢复后的首轮真实 CI 暴露一个问题:check:docs 发现 AUTO-GEN 的 content/docs/references/data/data-engine.mdx 未随 DroppedFieldsEventSchema 再生 — 已在 dae0221 修复(gen:schema && gen:docs 再生,纯增量 +17 行);
  • 当前 head(dae0221)16 个 check 全部通过:CI(Build/Test Core、Generated Artifacts、Dogfood Gate)、Lint & Type Check、CodeQL、Changeset/Size/Label、Spec liveness、Docs Drift、Vercel;
  • 此前两天的整片红灯确认为 GitHub-hosted runner 故障(全仓所有分支秒败、零日志),与本 PR 无关,不再赘述。

CI 已无阻塞,可以 review/merge。


Generated by Claude Code

@os-zhuang
os-zhuang merged commit b95577a into mainJul 24, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/update-record-silent-field-strip-l77rag branch July 24, 2026 12:12
os-zhuang added a commit that referenced this pull request Jul 24, 2026
…#3431) (#3448)
Wire the engine's onFieldsDropped strip-observability channel (#3413) through the DataProtocol and REST write path. updateData collects the engine's readonly/readonlyWhen strips; createData surfaces the #3043 static-readonly ingress strip via a payload diff. Both attach an optional droppedFields to the response. spec: Update/CreateDataResponseSchema gain optional droppedFields (backward-compatible). rest: PATCH/POST echo drops as the X-ObjectStack-Dropped-Fields header and keep the structured list on the body; status/success unchanged.
Deferred to a follow-up: bulk (updateManyData/createManyData/batchData) + GraphQL wiring, typed @objectstack/client warnings, CORS exposeHeaders.
os-zhuang added a commit that referenced this pull request Jul 24, 2026
…design time (#3425) (#3465)
A flow `update_record` node that writes a field the target object declares
`readonly: true`, under the default `runAs: 'user'`, is a silent no-op: the
objectql engine strips static-`readonly` fields from a non-system UPDATE
payload (#2948) so the write never lands, while the step still reports success.
#3407/#3413 surfaced the strip at run time; this shifts discovery left to
`os validate` / `os build`.
- New `@objectstack/lint` rule `validateReadonlyFlowWrites`: static readonly +
literal field under runAs!=='system' → error (gates); readonlyWhen → warning.
Skips create_record (INSERT is engine-exempt), runAs:'system' flows, templated
object names, and non-literal fields maps to stay false-positive-free.
- Wired into `os validate` and `os compile`/`os build` (mirrors the security
posture gate). Verified: reds on a violating app-crm flow, clean on all
example apps.
- Documents the formal contract in the objectstack-data / -automation skills:
readonly governs the user/API surface; system writers (runAs:'system', hooks,
seeds) maintain it.
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
baozhoutao added a commit that referenced this pull request Jul 28, 2026
…y` is enforced (#3835)
`POST /batch` called `ql.insert` directly, and the engine's INSERT path is
static-`readonly`-exempt by design (#3413) — the strip that stops a non-system
caller from seeding a read-only column lives at the protocol's create ingress
(#3043). So the same forged value was dropped on `POST /data/:object` and
written through on `/batch`: one rule, two answers.
Create ops now go through `p.createData`, the ingress itself, rather than a
second copy of the strip at the REST layer. One ingress means a future change to
its policy covers the batch for free, and the carve-outs it already encodes stay
intact — the platform-object exemption (a `sys_`/`managedBy` object's own guard
must REJECT a forged value, not silently swallow it) and the `isSystem`
exemption. `trxCtx` is passed as the context, so the insert still joins the batch
transaction and `$ref` resolution is unaffected; the ingress's `droppedFields`
fold into the batch's per-op list.
Update ops are untouched — the engine enforces both strips on its update path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 28, 2026
…cy, dropped fields, and the batch create ingress (#3794, #3835) (#3834)
* fix(approvals,rest): surface the node's lock policy and the batch's dropped fields (#3794)
An approval flow reported record writability wrong in both directions: what the
user could change said "locked", and what they couldn't said "updated
successfully". Both halves were missing signal, not wrong behaviour — the server
did the right thing and told nobody.
`rowFromRequest` now emits `locks_record`, read from the same `node_config_json`
snapshot the `beforeUpdate` lock hook reads, with the same default-true. A client
could previously only see "a request is pending" and had to guess whether that
locked the record; the Console guessed "locked" every time, which hides the whole
point of a `lockRecord: false` node.
`POST /batch` (cross-object transactional batch) never wired `onFieldsDropped`,
so the one write path the Console's master-detail record form takes was also the
one path with no write-observability — a `readonlyWhen` strip there was
completely silent. It now collects per-op events and returns them as a top-level
`droppedFields` list tagged with each operation's index; omitted when empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(spec): regenerate the batch reference for CrossObjectBatchDroppedFields (#3794)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(spec): refresh the API-surface snapshot for CrossObjectBatchDroppedFields (#3794)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(rest): route batch creates through the create ingress so `readonly` is enforced (#3835)
`POST /batch` called `ql.insert` directly, and the engine's INSERT path is
static-`readonly`-exempt by design (#3413) — the strip that stops a non-system
caller from seeding a read-only column lives at the protocol's create ingress
(#3043). So the same forged value was dropped on `POST /data/:object` and
written through on `/batch`: one rule, two answers.
Create ops now go through `p.createData`, the ingress itself, rather than a
second copy of the strip at the REST layer. One ingress means a future change to
its policy covers the batch for free, and the carve-outs it already encodes stay
intact — the platform-object exemption (a `sys_`/`managedBy` object's own guard
must REJECT a forged value, not silently swallow it) and the `isSystem`
exemption. `trxCtx` is passed as the context, so the insert still joins the batch
transaction and `$ref` resolution is unaffected; the ingress's `droppedFields`
fold into the batch's per-op list.
Update ops are untouched — the engine enforces both strips on its update path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
) (objectstack-ai#5627)
* fix(objectql): treat `autonumber` as implicitly readonly on the write path (objectstack-ai#5503)
A non-system REST caller could POST an explicit record number (bypassing the
sequence) and PATCH an existing one (forging a business identifier). The engine
already documented that "the runtime owns the value, not the client" — and both
record validators exempt a `required` autonumber because of it — but nothing on
the write path enforced it. Author-declared `readonly: true` fields were already
stripped (objectstack-ai#2948 / objectstack-ai#3043); `autonumber` carries no such flag, so the strip loop
walked straight past it. Same defect family as objectstack-ai#4447 (`created_at`).
- UPDATE: `stripReadonlyFields` now reads two sources of "read-only" at equal
rank — the author-declared flag and a runtime-owned field TYPE
(`isRuntimeOwnedField`, today exactly `autonumber`). Single-id and `multi`
bulk updates share this one strip site, so both are covered.
- INSERT: a narrower `stripRuntimeOwnedFields` runs in the engine before
`applyAutonumbers`. It deliberately does NOT take over the author-declared
`readonly` insert strip, which stays at the DataProtocol ingress per objectstack-ai#3413 /
objectstack-ai#3043 (a create may legitimately seed read-only columns, and trusted internal
writers call `engine.insert` directly).
Stripping in the engine, before dispatch, is what makes this driver-agnostic:
a driver advertising `supports.autonumber` receives a row with no caller value,
so its persistent sequence always wins — no driver changed. The tests assert
the driver-facing payload rather than patching a driver.
Exemptions keep their existing semantics: `isSystem` writes skip the pass, a
`preserveAudit` historical import may still reinstate legacy record numbers
(objectstack-ai#3493 whitelist — `autonumber` is an author-declared business field), and only
CALLER-supplied keys are candidates, so hook stamps survive.
The strip is silent, so it is reported: the insert path's `onFieldsDropped`
(objectstack-ai#3407) is wired at the new strip site as its standing note required, reusing
the existing `readonly` reason code rather than forking the spec vocabulary.
`createManyData` and `insertManyData` forward the listener too; the latter keeps
row precision by attributing each dropped name to the rows that supplied it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
* test(objectql): drop two `as any` erasures on ordinary engine reads (objectstack-ai#4918 ratchet)
`check:query-options-erasure` reported the test surface growing 267 -> 269: the
objectstack-ai#5503 test file passed `{ where: { id } } as any` to `findOne` and `{} as any`
to `find`. Neither is deliberately off-contract — both values are plain
`EngineQueryOptions` that the signatures already infer — so the gate's FIRST
remedy applies (drop the assertion), not the `as unknown as EngineQueryOptions`
spelling reserved for tests whose subject IS off-contract input.
The baseline is untouched: the count returns to its 267 ceiling on its own.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
* docs(changeset): record the strictReadonlyWrites superposition (objectstack-ai#5503 x objectstack-ai#5126)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

service-automation: update_record reports success when written fields are silently stripped — no observability for dropped writes (split from #3356)

2 participants

@os-zhuang@claude