Skip to content

docs(core): fieldRules 模块头收窄 readonly fail-open 的「与服务端一致」断言 (#3828) - #3887

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3828-fieldrules-docblock
Aug 9, 2026
Merged

docs(core): fieldRules 模块头收窄 readonly fail-open 的「与服务端一致」断言 (#3828)#3887
yinlianghui merged 1 commit into
mainfrom
claude/issue-3828-fieldrules-docblock

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3828

改了什么

只有一处:packages/core/src/evaluator/fieldRules.ts模块头注释。零行为改动 —— 代码一行未动,packages/core/src/evaluator 的测试前后都是 289 passed / 9 files(用例数逐个一致)。

改前(fieldRules.ts:23-27 @ 47f607854):

Evaluation is fail-open for visibility/required (…) and fail-open for readonly (a broken predicate leaves the field editable) — matching the server, which logs and allows the change through.

按分诊裁定的方向 1 收窄:多数 fault 两端一致 fail-open;未绑定根那一类服务端自 objectstack#4889 起是 fail-CLOSED,两端方向相反;相反是刻意的,按 ADR-0057 D10 以服务端为准;并写明调用方后果。

为什么这句话必须改

客户端这一侧本身没错 —— resolveFieldRuleStatereadonlyWhenfallback: false(fieldRules.ts:196),字段仍可编辑,这是 ADR-0057 D10「server enforces, client is courtesy」下的合理选择。错的只是「一致」这半句。

服务端侧证据(objectstack origin/main @ fec784863,packages/objectql/src/validation/rule-validator.ts):

事实 位置
readonlyWhen 未绑定根 → 记日志 treating the field as LOCKED :599-603
同一分支 return true(= LOCKED) :604
其他 fault 仍 fail-open:failed to evaluate — change allowed through + return false :606-607
isReadonlyWhenLocked 定义 :580
stripReadonlyWhenFields 定义 / delete 掉该 key :522 / :537
批量路径同源(stripReadonlyWhenFieldsMulti) :741 / :772,说明见 :723-728
该 carve-out 的设计说明「the UNBOUND-ROOT case is fail-CLOSED (#4889)」 :99-109
ADR-0057 D10 是这个方向的依据(「puts enforcement on the server」) :567-569
requiredWhen 两端仍都 fail-open(objectstack#4977 只绑 scope、不改语义) :117-141:1416-1430

于是真实链路和注释描述的正好相反:注释让人建立「客户端可编辑 + 服务端放行 = 值会落库」的模型,而实际是「客户端可编辑 + 服务端锁死丢弃 = 表单能改、保存报成功、值静默不落库」。新正文因此额外写了一段排障方向:往服务端锁死那边查(服务端的 treating the field as LOCKED 警告,加写响应的 droppedFields —— 后者证据 objectstack packages/client/src/index.ts:335,349),而不是查客户端谓词。

两处需要评审留意的判断

  1. ADR-0057 的编号在两仓撞号。 framework 的 ADR-0057 D10 才是「server enforces, client is courtesy」(docs/adr/0057-erp-authorization-core-business-units-and-scope-depth.md:422,PS-2 实现注记 :516-526 把 client-side gate 与 server-side teeth 讲得最清楚);而本仓的 docs/adr/0057-console-ai-chat-one-conversation-docked.md 是完全无关的文档。本仓读者按裸编号翻会翻到错的那份,所以新注释写成 "the framework's ADR-0057 D10 …(framework numbering; this repo's own ADR-0057 is an unrelated document)"。
  2. 下文那句 "log and allow" 加了限定词。 模块头第三段原本写 "This is the client half of the server's log and allow convention",引用的是服务端通用 fault 的消息(rule-validator.ts:606)—— 这句本身准确,但不加限定就会把上一段刚收窄的结论在两段之后抹平,使 docblock 自相矛盾。因此只加了「for the faults where the two ends DO agree」+ 标注它是 GENERIC-fault 消息。仍在 issue 划定的文件面(同一个模块头)内,但确实超出「只改那一句」的字面范围,故在此显式说明。

requiredWhen 半句与可见性半句按分诊结论未动(前者 objectstack#4977 两端都 fail-open,后者服务端根本不评估字段级 visibleWhen)。

验证


Generated by Claude Code

模块头把客户端 readonly fail-open 写成「matching the server, which logs and
allows the change through」。这句在写下时为真,但自 objectstack#4889 起对**未
绑定根**这一类已不成立:`readonlyWhen` 因为引用了本次写入没有绑定的 scope 根
(`parent.status == 'paid'` 而手里没有 master-detail 头)而 fault 时,服务端是
fail-CLOSED —— `isReadonlyWhenLocked` 记日志后 `return true`(LOCKED),
`stripReadonlyWhenFields` / `...Multi` 随即把该 key 从 UPDATE payload 中删除。
客户端这一侧本身没错(fallback: false,字段仍可编辑),错的是「一致」这半句:
两端在这一类上方向相反,而注释把它描述成同向。

收窄后的正文点明三件事:两端在多数 fault 一致、未绑定根一类相反;相反是按
framework 的 ADR-0057 D10(server enforces, client is courtesy)刻意为之,客
户端不猜「锁死」;以及调用方后果 —— 表单可编辑、保存报成功、值静默不落库,排
障要往服务端锁死的方向查(服务端 `treating the field as LOCKED` 警告 +写响应
的 `droppedFields`),而不是查客户端谓词。

`requiredWhen` 半句复核后仍准确(objectstack#4977 绑了 `parent` scope 但刻意
保留 fail-open 语义,两端都跳过),字段级 `visibleWhen` 服务端根本不评估,两处
均不动。下文引用的 "log and allow" 服务端消息标注为 GENERIC-fault 消息,以免
两段之后把刚收窄的结论又抹平。

零行为改动:纯 docblock,代码一行未动;`packages/core/src/evaluator` 测试前后
均 289 passed / 9 files;changeset 空 frontmatter 声明不发布(照 #3749 先例)。

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 9, 2026 12:04am

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-D8DbRrMg.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 482.39KB 106.34KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 230.82KB 56.70KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 117.21KB 30.27KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.17KB 58.82KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

✅ 验收(PM,session session_01GTRjn8xBqp75dk7kFupVRt)

实物核验:头 1917deeb9,2 文件(fieldRules.ts 模块头 +31/−4、空 frontmatter changeset);docblock diff 全读;trailer 0;零行为改动有前后 289/289 逐一一致背书。
CI 终态(独立复核):20 检查全部 completed,18 success + 2 skipped,零失败。

裁定要点:

  • 方向 1 执行到位且超出预期:不止收窄断言,还把调用方可见后果(表单能改、保存成功、值静默不落库 —— 往服务端 treating the field as LOCKEDdroppedFields 方向排障)写成第三段,这正是这段注释存在的意义。
  • 两处披露判断均采纳:① ADR-0057 两仓撞号 —— framework 的 D10 才是 server-enforces/client-courtesy,本仓同号 ADR 无关,显式标注编号归属防下一个读者跳错文档;② 第四段旧句 "log and allow" 不加限定会把刚收窄的结论抹平、令 docblock 自相矛盾 —— 同一模块头内的必要连带,加限定并区分 GENERIC-fault 与 unbound-root 两种日志消息,正确。
  • 引用证据链完整:每个符号/单号带 objectstack 侧 file:line(含 visibleWhen 是 per-option 而非 field-level 概念的论证 —— 「field-level」限定词有实测依据);requiredWhen/可见性两半按分诊未动。
  • 注释-only 的反向验证等价物(前后用例数逐一一致)如实,changeset 空 frontmatter 照先例。

转 ready 并挂 auto-merge。越界 #3888(同一陈旧断言在本仓 ADR-0036 的第二份副本 —— 更权威位置)归分诊席。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 00:11
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit a0693d5 Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3828-fieldrules-docblock branch August 9, 2026 00:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants