Tracking issue for end-to-end browser verification of the v17 (17.0.0-rc) train , per the release plan. A dedicated agent will work through this checklist against a running app (PORT=3000 pnpm dev showcase + :5180 objectui dev server where UI-side, or the dogfood-verification flow), acting as a real admin/user. Check items off as they are verified; file a separate issue for every failure found and link it here.
Defect rollups: #4482 (rc.0/rc.1 — 17 defects, all closed) · #5145 (rc.2/rc.3 — 3 defects, 2 open) . Individual issues are also linked from the item they block, below.
Sources of truth: the curated release page content/docs/releases/v17.mdx , the 300+ pending changesets in .changeset/, and the objectui window cf2d56e32a11 → 4a4829d0ef39 (incl. the backfilled range 2cb8d78e24ad...c6cfdf1288b6, see PR #3908 ). The docs-sweep log lives at docs/v17-docs-sweep.md.
A. Breaking behavior — verify the new posture actually enforces
Export opt-in (用户级 export 权限轴(接入 P1 预留的 userExportAllowed 槽) #3544 /定时报表的 CSV 附件绕过用户级 export 权限轴(#3544 的第二道出数据的门) #3710 ): as a member_default user, export on any list is denied; after an admin adds allowExport: true to the set, export works; viewAllRecords alone does NOT grant export; a set carrying allowExport cannot bind to everyone/guest anchors; export headers still emit on an empty result (security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547 ); reports covered by the same axis.
ApiMethod shrink (P2:ApiMethod 枚举收缩至 6 原语(breaking,独立版本) #3543 ): an object authored with a legacy value (e.g. apiMethods: ['upsert']) strips to deny-all with a FROM→TO warning; primitives-declared objects still derive search/export/history correctly; Console renders the server's effective operation set (objectui#2823) and detail/form edit & delete are gated on it (objectui#2832), matching grid rows (objectui#2889).
User-less flow runs refused (The #1888 user-less fail-open is wider than the lint that guards it — record-change flows fired by a system write run UNSCOPED, unlinted #3760 ): a runAs:'user' record-change flow fired by a system write (e.g. approvals status mirror) refuses data ops with UnscopedRunDataAccessError naming runAs:'system'; the originating write still succeeds; a runAs:'system' flow works and is audit-attributed.
Alias retirement ([P3] Retire the three deprecated aliases — via the ADR-0087 D2 conversion layer, not by deleting the keys #3855 ): authoring action.execute / field.conditionalRequired / agent.knowledge.topics is rejected with the rename in the error; os migrate meta --from 16 rewrites all three; agent.tools is tombstoned (parse error, not silent strip) (feat(spec,lint)!: accept ADR-0109, remove agent.tools[], lint agent authoring, model AI exposure in tool resolution (#3820) #3894 ).
Approval participant visibility (审批请求的可见性是「按租户」而非「按参与者」——getRequest 用 SYSTEM_CTX 读、只按 organization_id 收窄 #3590 ): a non-participant authenticated user cannot read another user's approval request (list/get/count), incl. payload snapshot, decision history, attachments; participants and admin override still can.
Sharing (fix(sharing): 「完全访问」声明了删除/转移/共享,实现上与「编辑」等价 #3865 /[P0] Metadata property liveness audit: ~half of all spec properties are dead; a cluster of security props is parsed-but-unenforced #1878 ): accessLevel:'full' converts to 'edit' at load; group recipient rejected (renamed team); business_unit recipient enforced; owner-type/guest rules no longer parse; the sharing-rule dialog is usable and permission-aware (objectui#2920); switching a rule off actually withdraws access (fix(sharing,driver-sql): a rule authored in Setup actually applies — and switching it off actually withdraws access #3850 ).
Membership grade closed (ADR-0108/sys_member.role app-role channel reverses ADR-0057 D4 ("never as the authority for RBAC") — pin to the built-ins before the next RC; placement is the governed channel #3723 ): a value in sys_member.role no longer projects into current_user.positions; sys_member writes are governed (ADR-0105 D8: the scope-bounded issuance path has no caller — every principal who may invite is already a tenant admin #3697 ); delegated_admin reachable in Console with narrowed pickers (objectui#2891); scoped invitations place into unit+positions (objectui#2868).
better-auth 1.7 (rc.2): existing password/social/SSO accounts sign in after the issuer backfill; unlink uses the account ROW id; boot log names non-derivable federated rows; 2FA obeys the operator lockout policy (2FA lockout is hardcoded to better-auth's defaults while the password lockout is operator-configurable — the second factor ignores the settings UI #3690 ); SCIM provider creation works when enabled (SCIM: 停在 @better-auth/scim rc.1,等正式版再整体迁移 —— rc.2 换掉了整套模型 #3653 ).
Datasource fail-fast (ObjectQLEngine.init() 吞掉 driver 连接失败:连不上数据库的服务器照常启动,driver 的启动期致命错误被降级成运行时报错 #3741 /显式绑定的 datasource 连不上只降级成一条 warning:绑上去的对象在启动后全废,直到查询时才炸 #3758 /ADR-0062 D1 收尾:default driver 的 connect 与失败判决仍是第二份实现(阻塞点已定位) #3826 ): a bound datasource with a bad URL fails the boot with an operator-readable error; /ready 503s when a driver stops answering; Setup datasource list shows the real connect verdict + reason (objectui#2926); startup banner reads DSN datasources and prints no credentials (启动横幅对 DSN 形式的 datasource 显示 → (unknown):describeRegisteredDriver 不认 connectionString #3793 ). — blocked on datasource: an object mapped to an unreachable datasource silently reads/writes the DEFAULT store — no boot failure, /ready still 200 #4462
Per-tenant unique (多租户缺陷:unique 物化为全局唯一索引、无视 tenancy,与按租户分裂的 autonumber 序列自相矛盾(跨租户必然撞号 + 存在性探测泄露) #3696 ): two tenants can hold the same unique value; violation message no longer leaks cross-tenant existence.
Multi-org entitlement (ADR-0105 D12): group posture refuses to activate without @objectstack/organizations; org switcher acts as write context with org attribution (objectui#2858).
Node 22 floor (CI validates every PR gate on Node 20 (EOL 2026-04-30) while release/publish runs Node 22 #3825 ): dev/serve/CLI on Node 22; EBADENGINE warning on Node 20.
B. Removed surfaces — verify they are actually gone (and loud)
/graphql → 404 (was 501); no GraphQL advertisement in discovery. — blocked on the discovery slot question below
SDK: client.ai.{nlq,suggest,insights}, client.permissions.*, client.realtime.*, client.workflow.*, client.views CRUD, notifications device/preference helpers, projects.listTemplates() — absent (compile error), not 404s.
CLI: os environments create --template rejected as unknown flag.
Console: API console lists the real AI family, tool preview links no 404 (objectui#2925/test(app-showcase): round-3 permission-model retest — issues register with evidence #2921 ); useClientNotifications no longer exposes device/preference delegates (objectui#2862). — blocked on Console approvals: quorum progress and per-group chips do not render, though the server sends minApprovals / group / __nodeLabel #4478 (quorum/group chips)
Marketplace publish moved: client.packages.install works (POST /packages no longer collides, POST /api/v1/packages is a publish-vs-install shape collision — REST registers first and shadows the dispatcher (#3587 finding) #3610 ); publish at POST /packages/publish.
C. New capabilities — verify they work end-to-end
Files as records (ADR-0104): upload against a field with accept/maxSize → violations rejected server-side (FileConstraintError, direct API call too, not just the widget); stored value is a sys_file id, read expands; downloads carry real filename+content-type (fix(approvals+storage): 审批决策附件在时间线里没名字、点击无反应、下载名是乱码 token (objectui #2820) #3504 ); governed download denies an unauthorized caller; os migrate files-to-references dry-run reports / --apply converts+verifies; strict mode only after the flag; Console adopts the reference shape incl. localized upload widget (objectui#2828/docs(adr): ADR-0092 标记 Accepted,关联实现 issue #2816 / #2817 #2819 ); image fields render consistently with click-to-zoom (objectui#2837).
Dynamic approver routing (审批流无法在「运行中」动态确定审批人——审批人在提交时即被 $trigger 快照锁定(动态路由 / 动态会签受阻) #3447 ): expression approvers resolve at node entry over current/trigger/vars (bare record rejected with prescriptive error); resolveAs + per_group sign-off; onEmptyApprovers all three policies (incl. auto_approve continuing the approve edge); decision outputs: declared keys accepted → resume as <nodeId>.<key> vars, undeclared rejected, typed outputs render record pickers (objectui#2831/docs+build(releases): whole-platform 12.0→14.x history + a maintenance model that includes the frontend #2827 ); live field/manager resolution lets an earlier step route a later one.
Approvals UX: quorum progress, per-group chips (objectui#2811), admin override for unstaffed approver (Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 , objectui#2810), timeline attachment chip opens (objectui#2821), lockRecord honored — "in approval (editable)" vs locked (objectui#2906/feat(cli): surface migration guide when specVersion trails the installed platform #2914 ), decision recorded against the authenticated caller (approvals: the acting identity must be resolved server-side, not taken from the request payload #3800 ), status mirror names the human (Approvals status mirror drops the acting user, forcing every downstream record-change flow to runAs:'system' #3783 ), dead run leaves no RECORD_LOCKED (Approval: a dead approval run can leave the record RECORD_LOCKED (#3424 follow-up, expected-behavior 3) #3456 ). — blocked on Console approvals: quorum progress and per-group chips do not render, though the server sends minApprovals / group / __nodeLabel #4478 / approvals: an admin override of a staffed approver slate is recorded as an ordinary approval — no override marker anywhere #4466
Write observability: PATCH/POST /data returns droppedFields (single, bulk, cross-object batch 审批场景下记录可写性的反馈全线失真:能改的显示「已锁定」,改不了的提示「更新成功」 #3794 ); Console toasts on silently-dropped read-only fields (objectui#2814) and keeps warnings on detail (objectui#2914); flow step warnings in Runs panel (service-automation: update_record reports success when written fields are silently stripped — no observability for dropped writes (split from #3356) #3407 ).
record-after-write trigger (record_change flow start node binds to a single lifecycle event — no create-OR-update in one flow #3427 ): fires on create OR update with previous null on create; Studio offers the new trigger and hides never-firing record-change.
Screen flows (Console: screen-flow Submit never calls the resume endpoint — every screen flow is un-completable from the UI #3528 ): pause → Console completes it; visibleWhen honored in render+validation; runner doesn't tear down its host. — blocked on screen flows: resume performs no server-side validation — missing required fields and undeclared keys all complete the run #4477
Analytics: RLS/tenant scope on ObjectQL path (security(analytics): ObjectQLStrategy 不消费 getReadScope — NativeSQL 回落后聚合查询无 RLS/租户谓词(#2852 修复未覆盖的另一半) #3597 ), dateRange applied (analytics: ObjectQLStrategy 静默忽略 timeDimensions[].dateRange —— 恰好是 date-granularity 图表必走的那条路 #3650 ), effective granularity in labels+drills (Analytics query builder ignores widget dateGranularity, sortBy/sortOrder, and funnel stage order #3588 ), FK-expand cross-object grouping / fail-closed (analytics/objectql: 跨对象(点号 lookup)维度聚合在 ObjectQL 路径上静默塌成 (null) 桶 / native SQL 报错 #3654 ), label lookups RLS-scoped (analytics: 让 executeAggregate 桥携带 ExecutionContext(#3597 的纵深防御第二层)+ 两处残留无 scope 调用 #3602 ), widget dateGranularity/sortBy/limit honored (objectui#2864), funnel stage order, {current_user_id} in widget filters (objectui#2857). — blocked on analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467
Historical import (data import: mid-lifecycle rows rejected by state_machine.initialStates — historical-data migration blocked (#3433 sibling) #3479 /data import: a "historical" import can't preserve original timestamps / audit fields — updated_at is stamped now, readonly fields stripped on upsert (#3479 follow-up) #3493 /data import undo: 撤销一次「historical」导入无法还原被保留的 readonly / 审计字段 — undo 的 writeCtx 缺 preserveAudit (#3493 follow-up) #3549 ): wizard "Import as historical data" (objectui#2815) skips state machine + preserves audit timeline, undo included; row errors human-readable, no raw SQL (Identity import: raw SQL leaks into row errors; DB constraint failures need sanitized, friendly messages #3566 ); email format validated in preview (objectui#2840).
i18n: /i18n/labels emits declared shape with help/options (GetFieldLabelsResponse declares rich label entries; both surfaces emit bare strings #3847 ); product-authored translations render (i18n: two unbridged bundle shapes — file-authored objects.<name> vs the runtime translation metadata type's o.<object>, so translations cannot be authored in the product #3778 ); all ten Console packs at key parity (objectui#2909); system-settings hub localized (objectui#2859); SSO/SCIM/user-position/import-job admin objects translated.
Seeds: boot banner Seeds: X inserted · Y updated · Z skipped + yellow REJECTED line (showcase: 4/5 projects and 9/10 tasks are silently rejected on every boot (FSM initialStates vs seed data) #3415 /os dev: surface seed / sample-data-heal outcomes in the boot summary (info logs are swallowed by the default warn log level) #3430 ); marketplace rehydrate heals sample data.
Grid/detail: eleven summary aggregations (objectui#2890), real match total under server pagination (objectui#2873), Kanban Uncategorized lane (objectui#2804), $or filters AND their contents (driver-sql), datetime grouping not collapsing to (null) on SQLite (SQLite 上 Field.datetime 的 dateGranularity 分桶恒为 NULL —— 趋势图塌成一根柱子 #3773 ), empty group bucket is real null (空分组桶的键两条路不一致:下推 SQL 给 null,内存兜底给 "(null)"(不限日期分桶) #3839 ).
D. Docs verification (site)
E. Known out-of-scope items already logged
Spec comment drift: core-services.zod.ts 'ai' member comment still says "NLQ, Chat, Suggest, Insights" (comment-only, spec-touching PR).
content/docs/releases/implementation-status.mdx predates the v17 window; needs its own regeneration pass.
RC exit: after changeset pre exit, re-check the v17 page's version wording and this checklist's remaining items.
F. 17.0.0-rc.1 supplement
Sections A–E were written against the rc.0 cut (watermark a641d10, just after the rc.0 version commit fc156fa4a). This section adds what shipped in 17.0.0-rc.1 (version commit 2bafe62e8, 2026-07-31): the window a641d10 → 2bafe62e8 , ~340 non-release commits, objectui pin advanced 4a4829d0ef39 → e651c936870e → a136322f8723 → bebaebd39ace → 7d9734d5e321. (Commits after 2bafe62e8 — #4421 /#4423 /#4424 /#4425 /#4427 /#4429 — are post-rc.1 and belong to the next rc's checklist.) Same rules: check off what passes, file a separate issue per failure and link it here.
F1. Actions — identity, dispatch, and failure semantics (ADR-0110 / ADR-0112)
Undeclared executables refused (feat(runtime,objectql,metadata)!: ADR-0110 — action identity is name; refuse undeclared executables (#3935) #3958 /feat(runtime)!: the undeclared-action refusal has no opt-out — drop OS_ALLOW_UNDECLARED_ACTIONS (ADR-0110 D3 revised) #3987 ): invoking an action name the object never declared is refused; the OS_ALLOW_UNDECLARED_ACTIONS opt-out no longer exists; action identity is name (Console carries the D1 fix, objectui a136322f8723).
Dispatch honors the declared type (fix(actions): dispatch on the declared action type over REST (#3915) #3919 /fix(actions): reach global actions at their real registration key, and 404 an action that never dispatched (#3913) #3930 /fix(actions): seed a flow action's params with the row id (#3915 follow-up) #3934 ): a flow action runs its flow (params seeded with the row id), a script action runs its body over REST; a global (object-less) action is reachable at its real key (/actions//:action, fix(actions): mount the object-less /actions//:action shape; bump objectui to e651c936870e #4005 ) — showcase ships a global-action specimen and a bulk selected-records action (feat(showcase): 演示作用于选中记录的批量 action #4201 ).
Failures speak HTTP (fix(actions)!: failures speak HTTP — rejections are 400, success is a single wrap (#3962) #3969 /fix(actions): an action that CRASHED is a 500, not a 200 reporting success:false (#3913 follow-up) #3951 /fix(runtime): serve VALIDATION_FAILED as 400 with fields[] from both dispatcher exits (#3918) #3920 ): a rejected action → 400 with the declared envelope; a CRASHED action → 500, never a 200 with success:false; VALIDATION_FAILED → 400 with fields[], and a form action anchors those field errors on the form controls across the sandbox boundary (fix(runtime): carry code/fields across the sandbox boundary so form actions can anchor validation errors (#3918 follow-up) #3937 ). — blocked on actions: sandbox capability denial answers 400 as a deliberate rejection, not the 500 the crash contract promises #4431
Error vocabulary is one catalog (ADR-0112, feat(spec,core,runtime,docs)!: ADR-0112 batch 1 — one error-code vocabulary, SCREAMING_SNAKE, schema-enforced (#3841) #3988 /refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003) #4021 /fix(runtime,spec)!: the dispatcher's error.code is the semantic string; the HTTP status moves to httpStatus (#3842) #3971 ): error.code is SCREAMING_SNAKE semantic string, HTTP status lives in httpStatus; field-validation messages are localized and name the field by its label (fix(spec,objectql,rest,runtime): localize field-validation messages, name the field by its label (#3957) #4014 ); no raw Zod shapes leak onto the wire (feat(spec,rest,objectql)!: a closed field-level error catalog, and Zod stops leaking onto the wire (#3977) #4035 ).
Action params enforced by default (feat(runtime)!: 动作参数默认严格,opt-in 开关删除(#3438 D2 翻转,17.0 车次) #4213 ): an out-of-contract param is refused with no opt-in flag; an action body writing ctx.record where it cannot land is visible at call time (feat(runtime): action body 被丢弃的 ctx.record 写在调用时可见 (#4345) #4357 ).
F2. Sharing & security hardening (ADR-0111)
F3. Query/write strictness — silent drops became refusals
List/query params: unknown list query params are rejected, not read as zero-matching filters (fix(data): reject unknown list query params instead of reading them as zero-matching filters (#4134) #4169 ); sort/select/expand naming a missing field are rejected (fix(data): sort / select / expand 指向不存在的字段时被拒绝,而不是静默丢弃 (#4226) #4240 ); a dotted-path sort is refused (fix(data): sort 的点号路径被拒绝(400 INVALID_SORT),不再静默降级为不排序 (#4256) #4303 ); searchFields/groupBy/aggregations naming a missing field are rejected (fix(data): searchFields / groupBy / aggregations 指向不存在的字段时被拒绝,而不是静默降级 (#4254) #4315 ); a filter the server cannot apply is rejected (fix(data): a filter the server cannot apply is rejected, not silently ignored (#4181) #4209 ), and an uncompilable filter throws instead of matching everything (fix(driver-sql,driver-memory): an uncompilable filter throws instead of matching everything (#3948) #4029 ).
Filter semantics: implicit field filters AND-merge with an explicit filter instead of being dropped (fix(data): implicit field filters AND-merge with an explicit filter instead of being silently dropped (#4164) #4190 ); a $filter array that is not a filter AST is rejected (fix(metadata)!: a $filter array that is not a filter AST is rejected, not passed through (#4121) #4200 ).
Writes: bulk writes bind to the object in the path , not the body (updateMany 让请求体覆盖路径上的 object —— 暴露闸门查的是 A,写进去的是 B #3933 ); deleteMany selects on the id list only (deleteManyData 把请求体的 options 展开在 where 之后 —— body 可覆盖 id 谓词,把"删这几条"放大成"删调用者可见的全部" #3897 ); the batch-size cap is enforced (批量写路由的 "max 200" 一处都没在拦 —— 而且同一个端点有两份互相矛盾的 Zod #3939 ). — blocked on data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435
Paging determinism (objectui#3106, fix(data): paging a sorted read is a partition of the result set, not five queries that share a WHERE clause (objectui#3106) #4367 /fix(batch,drivers): 分页确定性的两个后续 —— 把「有 gate」变成真的,把批处理翻页改成 keyset (#4363) #4416 ): paging a sorted grid is a partition — no duplicated/missing rows across pages; GET /data/:object/export honours a search term (fix(rest): GET /data/:object/export honours a search term #4230 ).
F4. Temporal correctness (ADR-0053 D-A3/D-D)
F5. Analytics honesty
F6. New capabilities since the rc.0 cut
Multi-value lookup seeds (seed-loader: 多值 lookup(multiple: true)的自然键数组被静默丢弃,记录落库但整个关联字段缺失 #3911 ): a seed row with a natural-key array on a multiple:true lookup resolves to references (showcase specimen); dropped reference fields are counted in the boot banner (seed-loader: a dropped reference FIELD is invisible in the summary — totalErrored counts dropped records only, so the boot banner reads clean #3932 ).
ADR-0113 required split (feat(spec,objectql,driver-sql)!: ADR-0113 accepted — required is a write contract, storage.notNull is the column constraint #4031 ): required enforces on write (yields fields[]), storage.notNull is the column constraint; the contradictory uniqueness double-declaration lints (feat(cli): lint the contradictory uniqueness double-declaration (#3991) #4034 ).
Approvals: decisionOutputs may be declared required (objectui#2955); structured reassign hand-off ([approvals] 转签审计默认 comment 落裸用户 ID("<from_id> → <to_id>"),审批动态里看不出谁转给谁 #4365 ) and flow system-write audit attribution ([automation/audit] runAs:'system' 流回写的审计行无归因(user_id/actor 双空),console 历史显示「未知用户」 #4366 ) via fix(automation,objectql,spec,approvals): flow system-write audit attribution (#4366) + structured reassign hand-off (#4365) #4403 ; per-run flow summaries selected / acted / skipped in Runs (feat(spec,service-automation): surface per-run flow summaries — selected / acted / skipped (#4354) #4377 ), with an honest marker when acted is incomplete (feat(spec,service-automation): a run says when its acted count is incomplete, instead of guessing (#4354) #4397 ).
UI metadata: FormSection.pane split-pane placement renders (feat(spec): FormSection.pane — explicit split-pane placement (objectui#2153 follow-up) #4160 ); element:button declares its action via InlineActionSchema (objectui#2997, feat(spec): element:button declares the action it executes — InlineActionSchema (objectui#2997) #4135 ); legacy string rowActions still render (objectui#2960, test(showcase): a specimen for legacy string rowActions (objectui#2960) #4101 ).
Files (ADR-0104 D3 wave 2): released-field-file collection runs behind the per-deployment migration gate (feat(storage): 开启回收 — 释放的字段文件按部署门禁进入墓碑与回收 — ADR-0104 D3 wave 2 (PR-5b) (#3459) #4208 ), proven end-to-end on a real boot (test(dogfood): 字段文件回收的真实启动端到端证明 — ADR-0104 PR-5b 全周期 + 两条否决路径 (#3459) #4285 ).
F7. Boot / CLI / authoring strictness
os serve boots the platform with no compiled artifact (fix(cli,core,metadata,runtime): the platform boots with no application — os serve no longer needs a compiled artifact (#4085) #4110 ); a config-booted app keeps its module code so action handlers reach the engine (fix(cli): a config-booted app keeps its module code — action handlers reach the engine again (#4095) #4129 , regression from serve 对「bare defineStack config + objects、无 dist artifact」的形态启动失败:Service 'manifest' is async - use await #3887 ); the boot merge keeps the authored api block (fix(cli): the boot merge no longer discards the authored api block (#4002) #4008 ); boot-quiet window buffers plugin logs instead of discarding them (fix(cli): serve's boot-quiet window buffers plugin log lines instead of discarding them (#4012) #4084 ).
os migrate value-shapes gates reference/structured-JSON value shapes per deployment (feat(migrate): os migrate value-shapes —— 引用/结构化 JSON 值形态的按部署门禁(#3438 第 2 项) #4235 ); migrate DDL is deferred until confirmed and SQLite occupancy is detected (fix(cli,driver-sql): defer migrate DDL until confirmed + detect SQLite occupancy (#3917) #3924 ); scaffolds stamp engines.protocol (fix(cli): stamp engines.protocol into scaffolds and example apps #4239 ).
Unknown-key rejection wave (未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 line): authoring an unknown key on flow/permission (feat(spec)!: 未知键在 flow / permission 授权 schema 上大声拒绝(#4001 Tier-A 切片) #4071 ), RLS/sharing/positions (feat(spec)!: RLS 策略 / 共享规则 / 岗位 schema 拒绝未知键(#4001 第 2 步) #4099 ), approvals (feat(spec)!: 审批授权 schema 拒绝未知键(#4001 第 3 步) #4119 ), app shell/navigation (feat(spec)!: 应用外壳与导航树拒绝未知键(#4001 app 步 PR B) #4165 ), hooks/datasources (feat(spec)!: hook 与 datasource 拒绝未知键(#4001 data 步,收尾最后两个待验证分类) #4207 ) fails validation loudly — incl. nested metadata (feat(spec): 未知键 lint 下探到嵌套元数据(补上 227 个此前无声的站点) #4218 ); the nested-select object form in select lists is removed (refactor(data)!: a select-list entry is a field name — the nested-select object form is removed (#4196) #4268 ); seven dead AppSchema keys are tombstoned (feat(spec)!: AppSchema 七个死键墓碑化(#4001 app 步 PR A) #4142 ).
Packaging: every published rc tarball carries CHANGELOG.md (chore(packaging): CHANGELOG.md ships in every npm tarball (#4261) #4297 ) and a files whitelist (chore(packaging): every published package declares a files whitelist (#4248) #4262 ); plugin-hono-server no longer serves CRUD/discovery (transport adapter only, feat(plugin-hono-server)!: 删除 CRUD/discovery 便利面 —— 插件回归纯传输适配器(#4073 终点) #4296 ) and registerStandardEndpoints defaults to false (feat(plugin-hono-server): registerStandardEndpoints 默认翻 false —— 便利面改为显式 opt-in(#4073 第一步) #4280 ).
F8. Docs (run 2+)
G. 17.0.0-rc.2 / rc.3 supplement
Sections A–E cover rc.0 , F covers rc.1 . This section covers the window 2bafe62e8 (rc.1) → a1a855a28 — 277 non-release commits, 283 new changesets, objectui pin 7d9734d5e321 → f5bc4c78be76. Two release cuts fall inside it: rc.2 (89d2a4eb3) and rc.3 (cut at 2e284b254, published as the npm rc dist-tag; main has since moved 56 commits past that point, and main's package.json still reads 17.0.0-rc.2 because the version commit was not merged back).
All 17 defects filed from the A–F verification are closed ; G1 re-verifies them rather than trusting the close.
Three new ADRs land in this window: ADR-0118 (non-user actor contract), ADR-0119 (reachable transactions + honest atomic batch), ADR-0121 (declarative-endpoint routing, rules only — implementation tracked separately).
G1. Regression — the A–F defects stay fixed
G2. ADR-0118 — the non-user actor contract
G3. ADR-0119 — atomic means atomic, or it refuses
G4. Hook / validation evaluation — the #4649 family
G5. Metadata plane honesty
A failed history read never invents event_seq / version ([metadata-protocol] SysMetadataRepository 的 nextEventSeq()/nextItemVersion() 同样把读失败当「表还没建」,静默从 1 重新发号 —— #4825 在 canonical 路径上的同形缺陷 #4867 , [metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825 ).
sys_metadata DDL failure is loud; only "table already exists" is swallowed ([metadata] database-loader 吞掉 sys_metadata 的 DDL 失败后仍置 schemaReady=true —— 第二类降级(#4632 规则),本轮因包冻结未修 #4728 ).
publishDraft's draft-drain discriminates by cause instead of swallowing every failure ([metadata-protocol] SysMetadataRepository.publishDraft() 把 draft 清理的全部失败都当「并发发布者已抽走」,静默留下一条永远 pending 的 draft 行 #4981 ).
Saved overlays are written through the SchemaRegistry and are dispatchable immediately (meta overlay: a just-saved overlay is listed but not dispatchable for a short window — a cache between saveMeta and resolveRouteActionDeclaration lags the write #4521 ) — no restart, no cache lag.
Audit history and global search sort by order, not direction (fix(metadata-protocol): two internal engine.find calls sort by direction, a key no driver reads — audit history and global search silently return ASCENDING #4674 ).
subscribeMetadata delivers true MetadataEvents (client realtime declared ≠ enforced:subscribeMetadata 回调把 RealtimeEventPayload 硬铸成 MetadataEvent,声明的顶层字段运行时是 undefined #4602 ); subscribeData delivers a real DataEvent (client realtime declared ≠ enforced:subscribeData 回调把 RealtimeEventPayload 硬铸成 DataEvent,顶层 recordId/changes/id 运行时是 undefined #4626 ).
G6. Seeds / boot / driver
G7. Authorization surfaces
G8. The #4001 / ADR-0049 close-out wave
This window is dominated by spec surface convergence — dual-source name collapses and inert-key retirements. Most are compile-time; verify the ones with a runtime face:
Retired datasource keys are refused at authoring: capabilities, healthCheck, retryPolicy, readReplicas (datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 , datasource.readReplicas 声明了、strict 了、刚被加了校验——但没有任何消费者(ADR-0049 enforce-or-remove,#4410 收尾发现) #4468 ).
connector.rateLimitConfig and the per-provider connector template cluster are refused (「v17」ConnectorRateLimitConfig 出站限流词表按 ADR-0049 摘除 —— 执行引擎不存在(#4686 拆向之二) #4911 , DatabaseConnector.readReplicaConfig 是读副本的第二处死声明(ADR-0049 enforce-or-remove,#4468 收尾发现) #4480 ).
dashboard.widgets[].responsive is refused (spec: DashboardWidgetSchema.responsive 按断点建模 —— objectui#3173 维护者已裁决;目标 17.0.0 正式版,带回退条款 #4876 ).
job is a code artifact — runtime creation and org overrides are withdrawn (#4488 审计发现的四个"授权门断连":email_template / job / validation 的元数据条目到不了执行点,action 导航项点不动 #4509 ). — FAILS: PUT /meta still creates job and agent → metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only #5086
composeStacks no longer silently drops top-level keys: same value passes, conflict errors, undeclared warns (composeStacks silently drops every non-array top-level key — api: today, server: as of #4910 #5005 ).
IDataDriver.findStream and the 31 inert DriverCapabilities bits are gone (IDataDriver.findStream 没有任何调用方,两个 driver 的实现还正好做了它承诺要避免的事(ADR-0049 enforce-or-remove) #4484 , DriverCapabilities.streaming 在 findStream 退休后指向不存在的能力,且从来没有读者(ADR-0049,#4484 收尾发现) #4634 ).
crypto.hash is retired across all four layers (crypto.hash 能力声明了、构建期还会自动推断,但沙箱从没实现 —— 调用直接抛(declared ≠ enforced) #4391 ).
The renamed spec exports resolve to exactly one declaration each (✅ spec 双源清账主单:基线 52 → 0(2026-08-03 收官)—— #4446 gate 落地后的偿还 worklist #4535 family: ActionLocation, ConflictResolution, EventSchema, HttpMethod, PackageDependency, FieldMapping, RateLimitConfig, MetadataFormat, CacheStrategy, notification vocabulary).
G9. Client / React
G10. Docs & close-out
Tracking issue for end-to-end browser verification of the v17 (17.0.0-rc) train, per the release plan. A dedicated agent will work through this checklist against a running app (
PORT=3000 pnpm devshowcase +:5180objectui dev server where UI-side, or the dogfood-verification flow), acting as a real admin/user. Check items off as they are verified; file a separate issue for every failure found and link it here.Sources of truth: the curated release page
content/docs/releases/v17.mdx, the 300+ pending changesets in.changeset/, and the objectui windowcf2d56e32a11 → 4a4829d0ef39(incl. the backfilled range2cb8d78e24ad...c6cfdf1288b6, see PR #3908). The docs-sweep log lives atdocs/v17-docs-sweep.md.A. Breaking behavior — verify the new posture actually enforces
member_defaultuser, export on any list is denied; after an admin addsallowExport: trueto the set, export works;viewAllRecordsalone does NOT grant export; a set carryingallowExportcannot bind toeveryone/guestanchors; export headers still emit on an empty result (security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547); reports covered by the same axis.apiMethods: ['upsert']) strips to deny-all with a FROM→TO warning; primitives-declared objects still derive search/export/history correctly; Console renders the server's effective operation set (objectui#2823) and detail/form edit & delete are gated on it (objectui#2832), matching grid rows (objectui#2889).runAs:'user'record-change flow fired by a system write (e.g. approvals status mirror) refuses data ops withUnscopedRunDataAccessErrornamingrunAs:'system'; the originating write still succeeds; arunAs:'system'flow works and is audit-attributed.action.execute/field.conditionalRequired/agent.knowledge.topicsis rejected with the rename in the error;os migrate meta --from 16rewrites all three;agent.toolsis tombstoned (parse error, not silent strip) (feat(spec,lint)!: accept ADR-0109, remove agent.tools[], lint agent authoring, model AI exposure in tool resolution (#3820) #3894).accessLevel:'full'converts to'edit'at load;grouprecipient rejected (renamedteam);business_unitrecipient enforced; owner-type/guestrules no longer parse; the sharing-rule dialog is usable and permission-aware (objectui#2920); switching a rule off actually withdraws access (fix(sharing,driver-sql): a rule authored in Setup actually applies — and switching it off actually withdraws access #3850).sys_member.roleapp-role channel reverses ADR-0057 D4 ("never as the authority for RBAC") — pin to the built-ins before the next RC; placement is the governed channel #3723): a value insys_member.roleno longer projects intocurrent_user.positions;sys_memberwrites are governed (ADR-0105 D8: the scope-bounded issuance path has no caller — every principal who may invite is already a tenant admin #3697);delegated_adminreachable in Console with narrowed pickers (objectui#2891); scoped invitations place into unit+positions (objectui#2868).defaultdriver 的 connect 与失败判决仍是第二份实现(阻塞点已定位) #3826): a bound datasource with a bad URL fails the boot with an operator-readable error;/ready503s when a driver stops answering; Setup datasource list shows the real connect verdict + reason (objectui#2926); startup banner reads DSN datasources and prints no credentials (启动横幅对 DSN 形式的 datasource 显示→ (unknown):describeRegisteredDriver 不认connectionString#3793). — blocked on datasource: an object mapped to an unreachable datasource silently reads/writes the DEFAULT store — no boot failure, /ready still 200 #4462unique(多租户缺陷:unique 物化为全局唯一索引、无视 tenancy,与按租户分裂的 autonumber 序列自相矛盾(跨租户必然撞号 + 存在性探测泄露) #3696): two tenants can hold the same unique value; violation message no longer leaks cross-tenant existence.groupposture refuses to activate without@objectstack/organizations; org switcher acts as write context with org attribution (objectui#2858).EBADENGINEwarning on Node 20.B. Removed surfaces — verify they are actually gone (and loud)
/graphql→ 404 (was 501); no GraphQL advertisement in discovery. — blocked on the discovery slot question belowclient.ai.{nlq,suggest,insights},client.permissions.*,client.realtime.*,client.workflow.*,client.viewsCRUD, notifications device/preference helpers,projects.listTemplates()— absent (compile error), not 404s.os environments create --templaterejected as unknown flag.useClientNotificationsno longer exposes device/preference delegates (objectui#2862). — blocked on Console approvals: quorum progress and per-group chips do not render, though the server sends minApprovals / group / __nodeLabel #4478 (quorum/group chips)client.packages.installworks (POST /packagesno longer collides, POST /api/v1/packages is a publish-vs-install shape collision — REST registers first and shadows the dispatcher (#3587 finding) #3610); publish atPOST /packages/publish.C. New capabilities — verify they work end-to-end
accept/maxSize→ violations rejected server-side (FileConstraintError, direct API call too, not just the widget); stored value is asys_fileid, read expands; downloads carry real filename+content-type (fix(approvals+storage): 审批决策附件在时间线里没名字、点击无反应、下载名是乱码 token (objectui #2820) #3504); governed download denies an unauthorized caller;os migrate files-to-referencesdry-run reports /--applyconverts+verifies; strict mode only after the flag; Console adopts the reference shape incl. localized upload widget (objectui#2828/docs(adr): ADR-0092 标记 Accepted,关联实现 issue #2816 / #2817 #2819); image fields render consistently with click-to-zoom (objectui#2837).expressionapprovers resolve at node entry overcurrent/trigger/vars(barerecordrejected with prescriptive error);resolveAs+per_groupsign-off;onEmptyApproversall three policies (incl.auto_approvecontinuing the approve edge); decision outputs: declared keys accepted → resume as<nodeId>.<key>vars, undeclared rejected, typed outputs render record pickers (objectui#2831/docs+build(releases): whole-platform 12.0→14.x history + a maintenance model that includes the frontend #2827); livefield/managerresolution lets an earlier step route a later one.lockRecordhonored — "in approval (editable)" vs locked (objectui#2906/feat(cli): surface migration guide when specVersion trails the installed platform #2914), decision recorded against the authenticated caller (approvals: the acting identity must be resolved server-side, not taken from the request payload #3800), status mirror names the human (Approvals status mirror drops the acting user, forcing every downstream record-change flow torunAs:'system'#3783), dead run leaves no RECORD_LOCKED (Approval: a dead approval run can leave the record RECORD_LOCKED (#3424 follow-up, expected-behavior 3) #3456). — blocked on Console approvals: quorum progress and per-group chips do not render, though the server sends minApprovals / group / __nodeLabel #4478 / approvals: an admin override of a staffed approver slate is recorded as an ordinary approval — no override marker anywhere #4466/datareturnsdroppedFields(single, bulk, cross-object batch 审批场景下记录可写性的反馈全线失真:能改的显示「已锁定」,改不了的提示「更新成功」 #3794); Console toasts on silently-dropped read-only fields (objectui#2814) and keeps warnings on detail (objectui#2914); flow step warnings in Runs panel (service-automation: update_record reportssuccesswhen written fields are silently stripped — no observability for dropped writes (split from #3356) #3407).record-after-writetrigger (record_change flow start node binds to a single lifecycle event — no create-OR-update in one flow #3427): fires on create OR update withpreviousnull on create; Studio offers the new trigger and hides never-firingrecord-change.visibleWhenhonored in render+validation; runner doesn't tear down its host. — blocked on screen flows: resume performs no server-side validation — missing required fields and undeclared keys all complete the run #4477dateRangeapplied (analytics: ObjectQLStrategy 静默忽略 timeDimensions[].dateRange —— 恰好是 date-granularity 图表必走的那条路 #3650), effective granularity in labels+drills (Analytics query builder ignores widget dateGranularity, sortBy/sortOrder, and funnel stage order #3588), FK-expand cross-object grouping / fail-closed (analytics/objectql: 跨对象(点号 lookup)维度聚合在 ObjectQL 路径上静默塌成 (null) 桶 / native SQL 报错 #3654), label lookups RLS-scoped (analytics: 让 executeAggregate 桥携带 ExecutionContext(#3597 的纵深防御第二层)+ 两处残留无 scope 调用 #3602), widgetdateGranularity/sortBy/limithonored (objectui#2864), funnel stage order,{current_user_id}in widget filters (objectui#2857). — blocked on analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467/i18n/labelsemits declared shape with help/options (GetFieldLabelsResponsedeclares rich label entries; both surfaces emit bare strings #3847); product-authored translations render (i18n: two unbridged bundle shapes — file-authoredobjects.<name>vs the runtimetranslationmetadata type'so.<object>, so translations cannot be authored in the product #3778); all ten Console packs at key parity (objectui#2909); system-settings hub localized (objectui#2859); SSO/SCIM/user-position/import-job admin objects translated.Seeds: X inserted · Y updated · Z skipped+ yellow REJECTED line (showcase: 4/5 projects and 9/10 tasks are silently rejected on every boot (FSM initialStates vs seed data) #3415/os dev: surface seed / sample-data-heal outcomes in the boot summary (info logs are swallowed by the default warn log level) #3430); marketplace rehydrate heals sample data.$orfilters AND their contents (driver-sql), datetime grouping not collapsing to(null)on SQLite (SQLite 上Field.datetime的 dateGranularity 分桶恒为 NULL —— 趋势图塌成一根柱子 #3773), empty group bucket is realnull(空分组桶的键两条路不一致:下推 SQL 给null,内存兜底给"(null)"(不限日期分桶) #3839).D. Docs verification (site)
/docs/releasesreachable from the left sidebar (restored in docs(releases,nav): v17 follow-up — Console gap backfill, sidebar restore, first docs sweep #3908) and/docs/releases/v17renders with Console section + upgrade checklist.docs/v17-docs-sweep.md(watermark:a641d10· 305 changesets · objectui4a4829d0ef39) — v17 is still iterating; append run 2+ to the log.E. Known out-of-scope items already logged
core-services.zod.ts'ai'member comment still says "NLQ, Chat, Suggest, Insights" (comment-only, spec-touching PR).content/docs/releases/implementation-status.mdxpredates the v17 window; needs its own regeneration pass.changeset pre exit, re-check the v17 page's version wording and this checklist's remaining items.F. 17.0.0-rc.1 supplement
Sections A–E were written against the rc.0 cut (watermark
a641d10, just after the rc.0 version commitfc156fa4a). This section adds what shipped in 17.0.0-rc.1 (version commit2bafe62e8, 2026-07-31): the windowa641d10→2bafe62e8, ~340 non-release commits, objectui pin advanced4a4829d0ef39 → e651c936870e → a136322f8723 → bebaebd39ace → 7d9734d5e321. (Commits after2bafe62e8— #4421/#4423/#4424/#4425/#4427/#4429 — are post-rc.1 and belong to the next rc's checklist.) Same rules: check off what passes, file a separate issue per failure and link it here.F1. Actions — identity, dispatch, and failure semantics (ADR-0110 / ADR-0112)
name; refuse undeclared executables (#3935) #3958/feat(runtime)!: the undeclared-action refusal has no opt-out — dropOS_ALLOW_UNDECLARED_ACTIONS(ADR-0110 D3 revised) #3987): invoking an action name the object never declared is refused; theOS_ALLOW_UNDECLARED_ACTIONSopt-out no longer exists; action identity isname(Console carries the D1 fix, objectuia136322f8723).flowaction runs its flow (params seeded with the row id), ascriptaction runs its body over REST; a global (object-less) action is reachable at its real key (/actions//:action, fix(actions): mount the object-less /actions//:action shape; bump objectui to e651c936870e #4005) — showcase ships a global-action specimen and a bulk selected-records action (feat(showcase): 演示作用于选中记录的批量 action #4201).success:false;VALIDATION_FAILED→ 400 withfields[], and a form action anchors those field errors on the form controls across the sandbox boundary (fix(runtime): carry code/fields across the sandbox boundary so form actions can anchor validation errors (#3918 follow-up) #3937). — blocked on actions: sandbox capability denial answers 400 as a deliberate rejection, not the 500 the crash contract promises #4431error.codeis the semantic string; the HTTP status moves tohttpStatus(#3842) #3971):error.codeis SCREAMING_SNAKE semantic string, HTTP status lives inhttpStatus; field-validation messages are localized and name the field by its label (fix(spec,objectql,rest,runtime): localize field-validation messages, name the field by its label (#3957) #4014); no raw Zod shapes leak onto the wire (feat(spec,rest,objectql)!: a closed field-level error catalog, and Zod stops leaking onto the wire (#3977) #4035).ctx.recordwhere it cannot land is visible at call time (feat(runtime): action body 被丢弃的 ctx.record 写在调用时可见 (#4345) #4357).F2. Sharing & security hardening (ADR-0111)
edit-level share does NOT grant delete.criteria_jsonis declaratively required (feat(sharing): criteria_json is declaratively required — ADR-0113 P2, the first consumer (P1 browser-verified) #4044); a disabled RLS policy no longer grants (fix(security)!: a disabled RLS policy no longer grants — security-subset liveness re-verification (#3896 follow-up) #3980). — blocked on sharing: deactivating a rule never withdraws its materialized grants — not on touch, not at boot #4433 / sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434api.requireAuthopt-out is gone;audience:'public'still publishes a book anonymously on a secure-by-default deployment (feat(rest): audience:'public' publishes a book anonymously on a secure-by-default deployment (#3963 step 1) #3995); with no auth implementation the/authdomain answers 501 instead of fabricating a login (fix(runtime)!: the /auth domain answers 501 instead of fabricating a login (#4113) #4179); dev security stubs refuse to load underNODE_ENV=production(fix(plugin-dev,plugin-hono-server): retire the security dev stubs; refuse to load under NODE_ENV=production (#4093) #4126).F3. Query/write strictness — silent drops became refusals
sort/select/expandnaming a missing field are rejected (fix(data):sort/select/expand指向不存在的字段时被拒绝,而不是静默丢弃 (#4226) #4240); a dotted-path sort is refused (fix(data):sort的点号路径被拒绝(400 INVALID_SORT),不再静默降级为不排序 (#4256) #4303);searchFields/groupBy/aggregationsnaming a missing field are rejected (fix(data):searchFields/groupBy/aggregations指向不存在的字段时被拒绝,而不是静默降级 (#4254) #4315); a filter the server cannot apply is rejected (fix(data): a filter the server cannot apply is rejected, not silently ignored (#4181) #4209), and an uncompilable filter throws instead of matching everything (fix(driver-sql,driver-memory): an uncompilable filter throws instead of matching everything (#3948) #4029).filterinstead of being dropped (fix(data): implicit field filters AND-merge with an explicit filter instead of being silently dropped (#4164) #4190); a$filterarray that is not a filter AST is rejected (fix(metadata)!: a$filterarray that is not a filter AST is rejected, not passed through (#4121) #4200).deleteManyselects on the id list only (deleteManyData把请求体的options展开在where之后 —— body 可覆盖 id 谓词,把"删这几条"放大成"删调用者可见的全部" #3897); the batch-size cap is enforced (批量写路由的 "max 200" 一处都没在拦 —— 而且同一个端点有两份互相矛盾的 Zod #3939). — blocked on data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435GET /data/:object/exporthonours asearchterm (fix(rest):GET /data/:object/exporthonours asearchterm #4230).F4. Temporal correctness (ADR-0053 D-A3/D-D)
Field.datetimestores one UTC form per dialect (fix(driver-sql): give Field.datetime one UTC storage form per dialect (#3912, #3942) #3953);Field.timestores wall-clockHH:MM:SStext (fix(driver-sql): Field.time 规范化存储 — HH:MM:SS[.fff] 墙钟文本,三方言统一 (#3994) #4020);Field.dateNOW()default reads the UTC calendar day (fix(driver-sql): Field.date 的 NOW() 默认值改读 UTC 日历日 (#4022);调高 cloud-connection seed-lookup 测试超时 #4026).<= 2026-07-31) covers the whole day on SQL, memory and mongo drivers (fix(driver-sql,service-analytics): bare-day upper bounds cover the whole day on Field.datetime (#3777) #4041/fix(driver-memory,driver-mongodb): bare-day upper bounds cover the whole day (#4042) #4048), and the RLS write-sidecheckevaluator honours the same calendar-day bounds (fix(formula,spec,core): the RLS write-sidecheckevaluator honours calendar-day upper bounds (ADR-0053 D-D) #4076).os migrate planlists the datetime storage convergence for a pre-v17 datastore (fix(cli,driver-sql): os migrate plan lists the datetime storage convergence (#3954) #3964).F5. Analytics honesty
/analyticsmounts only when the capability exists (feat(runtime): /analytics 路由按能力条件挂载——没装 service-analytics 连 405 都不再有(#3891 收尾,ADR-0076 D11) #4019); request bodies are validated at entry (fix(spec,runtime)!: AnalyticsQueryRequest 即裸 AnalyticsQuery;/analytics 入口 Zod 校验,畸形请求 400(#3878 建议 1) #4010). — blocked on analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467 / analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437COUNT(*)(#4157) #4184/fix(analytics): a new spec aggregate can no longer silently return a row count #4153);$or/$notfilters reach the query (fix(service-analytics): $or / $not filters reach the query (#4128 follow-up) #4175); the time axis is ordered by default andreport.orderis live (feat(analytics): order the time axis by default, give reports a sort declaration (#3916) #3921/chore(liveness): flipreport.orderto live now that objectui lowers it (#3916) #3938); atimeDimensionsbucket projects into result rows (fix(analytics): timeDimensions 分桶维度回到结果的 rows 与 fields (#4033) #4069).F6. New capabilities since the rc.0 cut
multiple:truelookup resolves to references (showcase specimen); dropped reference fields are counted in the boot banner (seed-loader: a dropped reference FIELD is invisible in the summary —totalErroredcounts dropped records only, so the boot banner reads clean #3932).requiredenforces on write (yieldsfields[]),storage.notNullis the column constraint; the contradictory uniqueness double-declaration lints (feat(cli): lint the contradictory uniqueness double-declaration (#3991) #4034).decisionOutputsmay be declared required (objectui#2955); structured reassign hand-off ([approvals] 转签审计默认 comment 落裸用户 ID("<from_id> → <to_id>"),审批动态里看不出谁转给谁 #4365) and flow system-write audit attribution ([automation/audit] runAs:'system' 流回写的审计行无归因(user_id/actor 双空),console 历史显示「未知用户」 #4366) via fix(automation,objectql,spec,approvals): flow system-write audit attribution (#4366) + structured reassign hand-off (#4365) #4403; per-run flow summariesselected / acted / skippedin Runs (feat(spec,service-automation): surface per-run flow summaries — selected / acted / skipped (#4354) #4377), with an honest marker whenactedis incomplete (feat(spec,service-automation): a run says when itsactedcount is incomplete, instead of guessing (#4354) #4397).FormSection.panesplit-pane placement renders (feat(spec):FormSection.pane— explicit split-pane placement (objectui#2153 follow-up) #4160);element:buttondeclares its action via InlineActionSchema (objectui#2997, feat(spec):element:buttondeclares the action it executes —InlineActionSchema(objectui#2997) #4135); legacy stringrowActionsstill render (objectui#2960, test(showcase): a specimen for legacy string rowActions (objectui#2960) #4101).F7. Boot / CLI / authoring strictness
os serveboots the platform with no compiled artifact (fix(cli,core,metadata,runtime): the platform boots with no application —os serveno longer needs a compiled artifact (#4085) #4110); a config-booted app keeps its module code so action handlers reach the engine (fix(cli): a config-booted app keeps its module code — action handlers reach the engine again (#4095) #4129, regression from serve 对「bare defineStack config + objects、无 dist artifact」的形态启动失败:Service 'manifest' is async - use await#3887); the boot merge keeps the authoredapiblock (fix(cli): the boot merge no longer discards the authored api block (#4002) #4008); boot-quiet window buffers plugin logs instead of discarding them (fix(cli): serve's boot-quiet window buffers plugin log lines instead of discarding them (#4012) #4084).os migrate value-shapesgates reference/structured-JSON value shapes per deployment (feat(migrate):os migrate value-shapes—— 引用/结构化 JSON 值形态的按部署门禁(#3438 第 2 项) #4235); migrate DDL is deferred until confirmed and SQLite occupancy is detected (fix(cli,driver-sql): defer migrate DDL until confirmed + detect SQLite occupancy (#3917) #3924); scaffolds stampengines.protocol(fix(cli): stampengines.protocolinto scaffolds and example apps #4239).CHANGELOG.md(chore(packaging): CHANGELOG.md ships in every npm tarball (#4261) #4297) and afileswhitelist (chore(packaging): every published package declares afileswhitelist (#4248) #4262);plugin-hono-serverno longer serves CRUD/discovery (transport adapter only, feat(plugin-hono-server)!: 删除 CRUD/discovery 便利面 —— 插件回归纯传输适配器(#4073 终点) #4296) andregisterStandardEndpointsdefaults to false (feat(plugin-hono-server): registerStandardEndpoints 默认翻 false —— 便利面改为显式 opt-in(#4073 第一步) #4280).F8. Docs (run 2+)
docs/v17-docs-sweep.mdfrom watermarka641d10→2bafe62e8; append the run entry.changeset pre exit: re-check the v17 release page wording and close out sections A–F.G. 17.0.0-rc.2 / rc.3 supplement
Sections A–E cover rc.0, F covers rc.1. This section covers the window
2bafe62e8(rc.1) →a1a855a28— 277 non-release commits, 283 new changesets, objectui pin7d9734d5e321 → f5bc4c78be76. Two release cuts fall inside it: rc.2 (89d2a4eb3) and rc.3 (cut at2e284b254, published as the npmrcdist-tag; main has since moved 56 commits past that point, and main'spackage.jsonstill reads17.0.0-rc.2because the version commit was not merged back).All 17 defects filed from the A–F verification are closed; G1 re-verifies them rather than trusting the close.
Three new ADRs land in this window: ADR-0118 (non-user actor contract), ADR-0119 (reachable transactions + honest
atomicbatch), ADR-0121 (declarative-endpoint routing, rules only — implementation tracked separately).G1. Regression — the A–F defects stay fixed
dimensions:["title"]returns only the titles they may read.DELETE /sharing/rules/:idOrNamesucceeds on both address forms and withdraws with it.created_at. A client-suppliedcreated_aton an ordinary PATCH is dropped (and reported indroppedFields), not persisted.lookupvalue resolving to no row is refused, includingsys_position_permission_set.permission_set_id.sys_approval_action.error.codeand no driver prefix; an analytics measure naming a missing field → 400 naming the field.6beb70856— overlays now write through the SchemaRegistry). — partially: namespace + shadowing + immediate dispatch fixed; the deleted-overlay-still-listed branch survives → meta overlays: a deleted overlay stays in the /meta listing though dispatch correctly 404s (#4432 residual) #5079lookupholding an embedded record object is reported by the scan.#4286removals are dated to 17.G2. ADR-0118 — the non-user actor contract
sys_metadata_history.recorded_bystores NULL for a non-user actor, not the sentinel string'system'(sys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556); readers tolerate both on rows written before the change.isSystemis explicit at every call site that means it.Tenancy:读布尔 multiTenant,与 ADR-0105 D1 的权威 knobOS_TENANCY_POSTURE对不上——isolated 会显示成 single-tenant #4801).G3. ADR-0119 — atomic means atomic, or it refuses
deleteMany/updateManywithatomic: trueeither run in one transaction or refuse — no partial application reported as success (fix(metadata-protocol): deleteManyData has the same fake-atomic as batchData, updateManyData ignores atomic entirely #4620).BatchOperationResultSchemashape (fix(metadata-protocol)!: batch 逐行结果迁移到BatchOperationResultSchema形状 —— 方案 B 已拍板,硬切 + 诚实迁移说明(Blocked-by #4620) #4793, hard cut); the newROLLED_BACK/NOT_ATTEMPTEDrow codes appear on the rows they describe.updateMany/deleteMany/ atomic all answerRECORD_NOT_FOUNDfor a row naming no recordG4. Hook / validation evaluation — the #4649 family
conditionthe platform cannot evaluate ABORTS the operation rather than defaulting to false (hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775) — the inverse of the pre-fix behaviour.previous, so transition semantics are expressible and aligned with validation predicates (bug(objectql/docs): hookcondition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784).readonlyWhenis enforced server-side, not only in the client (Parent-scopedreadonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889).G5. Metadata plane honesty
event_seq/version([metadata-protocol] SysMetadataRepository 的 nextEventSeq()/nextItemVersion() 同样把读失败当「表还没建」,静默从 1 重新发号 —— #4825 在 canonical 路径上的同形缺陷 #4867, [metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825).sys_metadataDDL failure is loud; only "table already exists" is swallowed ([metadata] database-loader 吞掉 sys_metadata 的 DDL 失败后仍置 schemaReady=true —— 第二类降级(#4632 规则),本轮因包冻结未修 #4728).publishDraft's draft-drain discriminates by cause instead of swallowing every failure ([metadata-protocol] SysMetadataRepository.publishDraft() 把 draft 清理的全部失败都当「并发发布者已抽走」,静默留下一条永远 pending 的 draft 行 #4981).order, notdirection(fix(metadata-protocol): two internalengine.findcalls sort bydirection, a key no driver reads — audit history and global search silently return ASCENDING #4674).subscribeMetadatadelivers trueMetadataEvents (client realtime declared ≠ enforced:subscribeMetadata 回调把 RealtimeEventPayload 硬铸成 MetadataEvent,声明的顶层字段运行时是 undefined #4602);subscribeDatadelivers a realDataEvent(client realtime declared ≠ enforced:subscribeData 回调把 RealtimeEventPayload 硬铸成 DataEvent,顶层 recordId/changes/id 运行时是 undefined #4626).G6. Seeds / boot / driver
Seed.envis enforced against the runtime environment — a seed scoped to another env does not load (fix(seed): enforce Seed.env against the runtime environment #4836).--allow-destructiveis never aimed at a framework-created index ([schema-drift] Fresh DB boots "drifted": detector can't read COALESCE index columns, then tells the operator to --allow-destructive away a legitimate unique index #4884).pnpm dev起永久 10 条 ERROR #4769).Driver already registered, skipping {"driverName":"com.objectstack.driver.sql"}—— SQL driver 被注册了两次 #4773).NOW()defaultValuetoken (data: theNOW()defaultValue token is unresolved on non-SQL datasources — the engine stamps the literal string, and the record validator then rejects the insert #4597).G7. Authorization surfaces
session.rolesadmin exemptions are gone from approvals — record lock and delegation guard (plugin-approvals 的 admin 豁免读session.roles,而 ObjectQL 的buildSession()从不填充它 —— 记录锁/delegation 守卫的 admin 覆盖在真实引擎路径上永不生效 #4839).bindApprovalLockHook对谓词式(multi)更新完全失效:if (!id) return把「没解析到行」当成「允许」 #4778).areas[], not only on the top-level tree (filterAppForUser 只走 app 顶层 navigation —— areas[] 里的 nav 项权限过滤仅客户端生效(#4651 移除假闸门后剩下的真缺口) #4722).app.areas[].visible/.requiredPermissionsare retired as fail-open gates (app.areas[] 的 visible / requiredPermissions 是 fail-open 的访问闸门 —— 服务端从不走 areas(ADR-0049,v17 限时) #4651) — authoring them is refused.active存储列喂进了 #4001 之后严格化的 permission spec #4669).action.bodybinds only fortype: 'script'(action.body 的 type 门是 declared ≠ enforced —— spec 说只在 type:'script' 生效,runtime 有 body 就绑 #4352).G8. The #4001 / ADR-0049 close-out wave
This window is dominated by spec surface convergence — dual-source name collapses and inert-key retirements. Most are compile-time; verify the ones with a runtime face:
capabilities,healthCheck,retryPolicy,readReplicas(datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583, datasource.readReplicas 声明了、strict 了、刚被加了校验——但没有任何消费者(ADR-0049 enforce-or-remove,#4410 收尾发现) #4468).connector.rateLimitConfigand the per-provider connector template cluster are refused (「v17」ConnectorRateLimitConfig 出站限流词表按 ADR-0049 摘除 —— 执行引擎不存在(#4686 拆向之二) #4911, DatabaseConnector.readReplicaConfig 是读副本的第二处死声明(ADR-0049 enforce-or-remove,#4468 收尾发现) #4480).dashboard.widgets[].responsiveis refused (spec:DashboardWidgetSchema.responsive按断点建模 —— objectui#3173 维护者已裁决;目标 17.0.0 正式版,带回退条款 #4876).jobis a code artifact — runtime creation and org overrides are withdrawn (#4488 审计发现的四个"授权门断连":email_template / job / validation 的元数据条目到不了执行点,action 导航项点不动 #4509). — FAILS: PUT /meta still creates job and agent → metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only #5086composeStacksno longer silently drops top-level keys: same value passes, conflict errors, undeclared warns (composeStackssilently drops every non-array top-level key —api:today,server:as of #4910 #5005).IDataDriver.findStreamand the 31 inertDriverCapabilitiesbits are gone (IDataDriver.findStream 没有任何调用方,两个 driver 的实现还正好做了它承诺要避免的事(ADR-0049 enforce-or-remove) #4484, DriverCapabilities.streaming 在 findStream 退休后指向不存在的能力,且从来没有读者(ADR-0049,#4484 收尾发现) #4634).crypto.hashis retired across all four layers (crypto.hash 能力声明了、构建期还会自动推断,但沙箱从没实现 —— 调用直接抛(declared ≠ enforced) #4391).ActionLocation,ConflictResolution,EventSchema,HttpMethod,PackageDependency,FieldMapping,RateLimitConfig,MetadataFormat,CacheStrategy, notification vocabulary).G9. Client / React
client-reacthooks no longer loop on dependency identity (useQuery 在文档示例用法下是无界请求循环:内联 options 300ms 内发出 8830 次 find #4693, useAutoRefresh 在 refetch 身份不稳定时每渲染一次就重订阅一次(两条流都churn) #4694).@objectstack/organizationsresolves from the host app, and an organizations failure is classified by STAGE — import ≠ mount (serve.ts 的 ADR-0093 D5 catch 把 OrganizationsPlugin 抛出的任何错误都改写成「包加载不出来」,并给出会误导的 OS_ALLOW_DEGRADED_TENANCY 解法 #4818, cloud#1013).req.usercarries the capability channel (AI 路由的 req.user 丢失 capability 通道 —— ec.systemPermissions 从不透传,AI 域无法做任何 capability gate #4705).G10. Docs & close-out
2bafe62e8→a1a855a28, adding the retired keys from G8 to the fingerprint table, plus the version-number check docs: nine passages date the #4286 query-surface removals to spec 18 — they ship in 17 #4476 called for.changeset pre exit: re-check the v17 page's version wording and close out sections A–G.