Repository navigation
v17 verification tracker — browser-verify every user-facing change in the 17.0.0 train #3909
Description
Activity
rc.1 verification run 1 — section F
Environment: framework
main@1ee48bc60(post-rc.1), showcase example underos serve --dev --uion an isolated SQLite datastore,@objectstack/console@17.0.0-rc.1(the published vendored SPA, pin7d9734d5e321) at/_console/, Node 22.22. Driven asadmin@objectos.aiplus a second signed-up member for every two-principal check.19 items ticked. Highlights of what actually held up under a live request:
- Actions (F1). An undeclared action name is refused with the prescriptive ADR-0110 404;
scriptruns,flowdispatches (returnsstatus:"paused"with the screen payload), and aurl/modalaction reaching the server is refused with a message naming its client-side nature. Param enforcement is strict by default and precise: missing required → 400 naming the param, unknown key → "Unknown action param … not declared on this action", bad option → the allowed list. Field validation localizes properly (Accept-Language: zh-CN→ 优先级必须是以下值之一) and carries per-fieldfields[]with labels. - Sharing (F2). The ADR-0111 verb boundary is real: an
edit-level share lets the recipient read and PATCH, and refuses DELETE (403) and share-management ("requires record ownership or Modify All Data (ADR-0111 D1)"). A criteria-less rule is refused with the ADR-0049 message. A non-participant cannot read an approval request (404). Anonymous data access is denied on both read and write, whileaudience:'public'book metadata is served without a session. - Query strictness (F3). Every silent-drop this train closed refuses correctly, each with an actionable message: unknown query param,
sort/select/expand/dotted-path sort naming a missing field, non-AST$filter. Implicit field filters AND-merge with an explicitfilter(verified by counts: 2 ∧ 1 → 1).deleteManyis ids-only (awherebody is rejected), the batch cap fires at 200, and sorted and unsorted paging are exact partitions (page union == full set, no dupes/gaps). - Temporal (F4).
+08:00input stores as UTC…T10:30:00.000Z,timestores wall-clock09:15:30, and a bare-day upper bound ($lte: '2026-08-15') covers the whole day on bothdateanddatetime. - Analytics (F5).
/analyticsvalidates at the entry (unknown body key rejected by name), asummeasure emitsSUM(total)notCOUNT(*),$or/$notreach the SQL, and a monthlytimeDimensionsbucket projects into rows in date order. - New capabilities (F6). The multi-value lookup seed resolves (
f_lookups: ['Northwind','Contoso']→ a two-id array).requiredenforces on write with per-field errors, and the Console form shows the same three fields as required. The approval chain worked end to end in the browser and over REST: submit → request created → structured reassign to a second user → that user approves →status: approved, andsys_approval_actionattributes each decision to the acting user (submit/reassign → admin, approve → member). The screen flow ran from the list toolbar, collected input, and the write landed (assignee= the value typed in the dialog). - Boot/CLI (F7).
os serveboots a stack with no compiled artifact; the undeclared-stack-key lint reports each key with a remedy (including thestorage→OS_STORAGE_*guidance); rc.1 tarballs carryCHANGELOG.mdand afileswhitelist (@objectstack/clientships 10 files).
Failures filed (7)
# Area What #4431 actions a sandbox capability denial answers 400 as a deliberate rejection — the crash contract (#3951) says 500 — and leaks the SandboxError:debug prefix#4432 metadata /meta/<type>overlay writes don't normalize the type segment: plural vs singular land in different namespaces, one overlay row shadows the whole code-authored listing, and the row is undeletable#4433 sharing deactivating a rule never withdraws its materialized grants — not on record touch, not after a full restart #4434 sharing DELETE /sharing/rules/:idOrName500s for both address forms — no sharing rule can be deleted over REST (compounds #4433: an over-granting rule is unrecoverable)#4435 data PATCH/DELETEof a nonexistent record answer 200 success (record: null/success: true) whereGETcorrectly 404s;deleteManyreports every typo'd id as deleted#4436 data the unsupported-filter-operator refusal ships without error.codeand leaks the[sql-driver]prefix (ADR-0112)#4437 analytics a measure naming a missing field 500s with SQLITE_ERRORinstead of a 400 naming the fieldNotes for the next run
- Two toast strings in the Console are still English on a zh-CN session ("This flow runs on a single record — select a row first.", "Done") — worth a pass against the objectui i18n packs before RC exit rather than filing per-string.
- The showcase's own
devscript resolves a stale global@objectstack/cli@9.0.0from the user PATH instead of the workspace CLI, which fails the boot with a misleading "plugin-approvals … not installed". Not a platform defect; worth a line in the contributor docs since it costs a debugging cycle. - Not yet covered in this run and still open in section F: the file/attachment axis (F6
#4208/#4285gate), flow run summaries (selected/acted/skipped),os migrate value-shapes, and the F8 docs sweep.
- Actions (F1). An undeclared action name is refused with the prescriptive ADR-0110 404;
rc.0 verification run 2 — sections A / B / C
Run 1 covered only the F supplement. This run works the rc.0 checklist (A–D) on the same environment (framework
main@1ee48bc60, showcase underos serve --dev --ui, published@objectstack/console@17.0.0-rc.1, isolated SQLite, admin + a second signed-up member). 7 more items ticked — 26 of 59 total. Three new defects filed.A. Breaking behavior
- Export opt-in (用户级 export 权限轴(接入 P1 预留的 userExportAllowed 槽) #3544/定时报表的 CSV 附件绕过用户级 export 权限轴(#3544 的第二道出数据的门) #3710/security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547) — all five branches hold. A
member_defaultuser is refused (EXPORT_NOT_PERMITTED403). Binding a set carryingviewAllRecordsonshowcase_invoicelets that user read every invoice (200) while export stays 403 — read genuinely no longer implies export. AddingallowExport: trueto that one object flips export to 200, and a second object in the same set (noallowExport) stays 403, so the grant is per-object, not per-set. An empty result still emits the full header row pluscontent-disposition/x-export-format/x-export-limit. The anchor gate refuses botheveryoneandguest, and — checked with a set carrying onlyallowRead + allowExport, no VAMA — names the right reason: "it carries bulk export on 'showcase_announcement' (ADR-0090 D5/D9)". - ApiMethod shrink (P2:ApiMethod 枚举收缩至 6 原语(breaking,独立版本) #3543) — the authoring half is exemplary.
enable: { apiMethods: ['upsert','aggregate'] }produces a per-value FROM→TO mapping (upsert: declare ['create','update'],aggregate: declare ['list']), the deny-all consequence spelled out ("After stripping, this whitelist is EMPTY —[]means DENY-ALL"), and a codemod path. AuthoringapiMethodsat object root is separately caught as an undeclared key. - Alias retirement ([P3] Retire the three deprecated aliases — via the ADR-0087 D2 conversion layer, not by deleting the keys #3855/feat(spec,lint)!: accept ADR-0109, remove agent.tools[], lint agent authoring, model AI exposure in tool resolution (#3820) #3894) — all four, and the migration runs.
field.conditionalRequired→requiredWhenandaction.execute→targetare hard parse errors naming the rename;agent.toolsandagent.knowledgeare tombstoned with a paragraph each on what to do instead (not a silent strip).os migrate meta --from 16reports both rewrites and--outproduces a snapshot carryingrequiredWhen/target. Note the shape: migrate rewrites the stack snapshot, it does not edit your TS source — worth stating in the upgrade guide, since the checklist wording ("rewrites all three") reads as an in-place codemod. - Approval participant visibility (审批请求的可见性是「按租户」而非「按参与者」——getRequest 用 SYSTEM_CTX 读、只按 organization_id 收窄 #3590). A non-participant authenticated user gets 404 on another user's request; after a structured reassign makes them a participant, 200.
B. Removed surfaces
- SDK removals verified by instantiating the client, not by grep (grep is misleading here —
nlq/suggest/insightsstill appear in comments documenting their removal). On a liveObjectStackClient:permissions,realtime,workflow,views,projects.listTemplatesare allundefined;client.aiexposes onlychat, chatStream, complete, models, conversations, agents, pendingActions;client.notificationsexposes onlylist, markRead, markAllRead(no device/preference delegates). - Marketplace move (POST /api/v1/packages is a publish-vs-install shape collision — REST registers first and shadows the dispatcher (#3587 finding) #3610).
POST /packagesis the install surface again ({"code":"VALIDATION_ERROR","message":"Package id is required"}— reachable, no collision). /graphql→ 404 ✅, but discovery still advertises the slot:services.graphql = {"enabled":false,"status":"unavailable","message":"No implementation ships for the 'graphql' slot — register a service under it to enable"}. That message invites someone to fill a slot for a surface v17 deliberately removed, and it contradicts the docs sweep's own run-1 fix (services-checklist was reduced to 16 services with every graphql row deleted). Left unticked pending a call on whether the slot should be dropped fromCORE_SERVICE_PROVIDERSor the message reworded.- Not verifiable here:
POST /packages/publishanswers 405 (Allowed: DELETE, GET, HEAD, PATCH) — the route-manager's/packages/:idmatches withid="publish"and shadows it. The ledger declares the publish route assource: 'direct-mount', disposition: 'server-only', so it is presumably registered only where the marketplace publish plugin is loaded. Worth confirming on a deployment that has it — the shadowing means route-registration ORDER decides the winner.
New defects (3)
# Area What #4441 data / RBAC a lookupaccepts an id that does not exist in the referenced object — includingsys_position_permission_set.permission_set_id, so a grant row can point at nothing while the audience-anchor gate has to resolve that set to judge it. Found by accident: a shell variable expanded toFAILand the platform stored it against theeveryoneposition.#4447 data created_atis client-writable on an ordinary PATCH and persists (1999-01-01written and read back).updated_atandcreated_byare correctly protected — this is one field of the audit trio. Its metadata saysreadonly: false.— — (the two #3909 F-section notes from run 1 stand) While chasing #4447 I could not find any input on this build that produces a non-empty
droppedFields: computed fields (f_formula,f_summary) are rejected 400INVALID_FIELD, unknown fields 400, and system fields are simply written. The #3794 contract may only have live coverage on the FLS path — worth a targeted check before RC exit rather than assuming the C-section item passes.Remaining
33 unchecked. The bulk needs setup this run did not build: a
runAs:'user'record-change flow fired by a system write (#3760), a per-tenantuniquespecimen (#3696), multi-org / membership-grade (ADR-0108, ADR-0105), better-auth 1.7 account migration, datasource fail-fast (#3741), the files-as-records axis (ADR-0104), historical import, and the Console-side items (objectui#28xx). Section D (docs) is untouched.- Export opt-in (用户级 export 权限轴(接入 P1 预留的 userExportAllowed 槽) #3544/定时报表的 CSV 附件绕过用户级 export 权限轴(#3544 的第二道出数据的门) #3710/security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547) — all five branches hold. A
rc.0 + rc.1 verification run 3 — the remaining sections
Continuing from runs 1–2. Same environment; this run built a dedicated probe stack (
examples/rc1-probe, removed afterwards) for the items the showcase has no specimen for, plus a synthetic pre-v17 datastore for the migration items.40 of 59 ticked (was 26). Five new defects filed, one of them the most serious found across all three runs.
🔴 #4467 —
/analytics/queryignores record-level scopingA plain member with shares on 2 of 5 private notes:
GET /data/showcase_private_note → total 2 ✅ POST /analytics/query {cube, measures:[count]} → count 5 ❌ POST /analytics/query {…, dimensions:["title"]} → returns the TITLES of all 5, including the 3 they cannot readAny authenticated caller who can reach
/analyticscan enumerate field values of every row of any cube-exposed object, past OWD, sharing rules and RLS. The other analytics items on the checklist (#4019 capability mount, #4010 entry validation, #4184 declared aggregates, #3650dateRange, #3921 time ordering) all hold — the scoping layer is the piece that is absent; the returneddata.sqlcarries only the caller's own predicates and no owner/share filter.Method warning for whoever re-tests: a
viewAllRecordsset left bound from an earlier test makes both paths agree at 5/5 and look correct. Confirm the data path returns fewer rows than admin before reading anything into the analytics numbers — I got a false negative from exactly this.Other new defects
# What #4462 an object mapped to an unreachable datasource silently reads/writes the DEFAULT store — boot succeeds, brokennever appears in the log,/ready200, listing saysstatus:"unvalidated". Against all three claims of the A5 item.#4466 an admin override of a properly-staffed approver slate is recorded as an ordinary approval — sys_approval_actionhas no override column at all, so bypassing designated approvers is byte-identical to being one.#4455 os migrate value-shapesmisses alookupholding an embedded record object — the exact case its own docstring names — becauseReferenceIdValueSchemaisz.string().min(1). The gate then closes on evidence it never gathered.#4441 a lookupaccepts an id that exists nowhere, including the RBAC permission-set link tables.Verified this run
- Export axis (用户级 export 权限轴(接入 P1 预留的 userExportAllowed 槽) #3544/定时报表的 CSV 附件绕过用户级 export 权限轴(#3544 的第二道出数据的门) #3710/security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547), all five branches. The decisive one: with
viewAllRecordsbound, the member reads every invoice (200) while export stays 403.allowExportflips it per-object; a second object in the same set stays denied. Empty result still emits the header row +content-disposition. The anchor gate refuseseveryone/guestand — tested with a set carrying onlyallowExport, no VAMA — names the right reason ("carries bulk export"). - 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 user-less flow runs, all three assertions. A
runAs:'system'flow's write fired arunAs:'user'flow; the latter was refused with theUnscopedRunDataAccessErrornamingrunAs:'system', the originating write still succeeded, and the sibling system flow ran normally. - record_change flow start node binds to a single lifecycle event — no create-OR-update in one flow #3427
record-after-write. One flow, both legs:previousisnullon create and the full prior snapshot on update. - 多租户缺陷:unique 物化为全局唯一索引、无视 tenancy,与按租户分裂的 autonumber 序列自相矛盾(跨租户必然撞号 + 存在性探测泄露) #3696 per-tenant unique. The index is literally
UNIQUE (organization_id, sku); two tenants hold the same SKU, same-tenant duplicate is 409, and the message leaks no cross-tenant existence. - 审批流无法在「运行中」动态确定审批人——审批人在提交时即被 $trigger 快照锁定(动态路由 / 动态会签受阻) #3447 dynamic approver routing, end to end. Required
decisionOutputsenforced on approve; an undeclared key rejected and the declared keys listed; stage 2 spawned its own request whosepending_approverswas exactly the user stage 1's lead named —expressionresolution overvars.lead_review.next_reviewersworks.lockRecordheld during, released after (Approval: a dead approval run can leave the record RECORD_LOCKED (#3424 follow-up, expected-behavior 3) #3456). - ADR-0104 files, full chain. A PNG bound to
accept:['application/pdf']→ 400 naming field and allowed types (direct API, not the widget); oversize → 400 naming both sizes; valid → stored as asys_fileid that reads back expanded to{id,name,size,mimeType,url}; download carriesfilename="ok.pdf"+application/pdf; anonymous download 401.os migrate files-to-referencesdry-run reports honestly and writes nothing. - P2:ApiMethod 枚举收缩至 6 原语(breaking,独立版本) #3543 apiMethods shrink — per-value FROM→TO mapping, the deny-all consequence spelled out, codemod path given. [P3] Retire the three deprecated aliases — via the ADR-0087 D2 conversion layer, not by deleting the keys #3855/feat(spec,lint)!: accept ADR-0109, remove agent.tools[], lint agent authoring, model AI exposure in tool resolution (#3820) #3894 aliases — all four hard-rejected with renames;
os migrate meta --from 16 --outproduces a snapshot carryingrequiredWhen/target. - fix(cli,driver-sql): os migrate plan lists the datetime storage convergence (#3954) #3964 / fix(cli,driver-sql): defer migrate DDL until confirmed + detect SQLite occupancy (#3917) #3924 —
migrate planlistsnormalize_datetime_storage: created_at — 2 row update(s); destructive index changes gated behind--allow-destructive; occupancy detection names the holding pid. - ADR-0105 D12 —
OS_TENANCY_POSTURE=groupwithout@objectstack/organizationsis a FATAL refusal with three remedies. ADR-0108 —sys_member.roleis a four-value closed select,sys_member/sys_invitationare read-only over REST; theowner→org_ownerprojection is the deliberate ADR-0095 D3 boundary, not the retired channel. - 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 seeds — a seed row with a bad reference turns the banner yellow:
⚠ Seeds: com.rc1.probe 2 ok / 1 error ⚠+ theOS_LOG_LEVEL=infohint. - Console/docs — Kanban grows a
未分类lane for a null-status row (objectui#2804);totalstays the match count under a 3-row page (objectui#2873); SQLite datetime grouping buckets correctly (SQLite 上Field.datetime的 dateGranularity 分桶恒为 NULL —— 趋势图塌成一根柱子 #3773) and an empty bucket is a realnull(空分组桶的键两条路不一致:下推 SQL 给null,内存兜底给"(null)"(不限日期分桶) #3839);/docs/releases/v17renders with the Console section and Upgrade checklist, reachable from the sidebar (docs(releases,nav): v17 follow-up — Console gap backfill, sidebar restore, first docs sweep #3908).
Two checklist-wording corrections
- Seeds. The item promises
Seeds: X inserted · Y updated · Z skipped. That shape does not exist —printSeedSummarydeliberately collapses the three into oneN rows/N ok / M errorscount and uses·to separate sources. The code's contract is coherent; the checklist text is stale. os migrate meta. "rewrites all three" reads as an in-place codemod. It rewrites the stack snapshot (--out), not your TypeScript sources.
Still open (19)
Mostly Console-side objectui items needing UI-level assertions (API console AI family, dropped-field toasts, quorum chips, historical-import wizard),
better-auth 1.7account migration (needs pre-existing social/SSO rows), the four items blocked on the defects above, and the two close-out items that can only run atchangeset pre exit.Also unresolved from run 2: I still cannot produce a non-empty
droppedFieldson any input — computed fields 400, unknown fields 400, system fields are written. #3794's contract may only have live coverage on the FLS path.- Export axis (用户级 export 权限轴(接入 P1 预留的 userExportAllowed 槽) #3544/定时报表的 CSV 附件绕过用户级 export 权限轴(#3544 的第二道出数据的门) #3710/security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547), all five branches. The decisive one: with
Final run — the checklist is worked through
48 of 59 verified. The remaining 11 are now annotated inline: 10 are blocked on defects filed from this verification (they cannot be ticked until a fix lands), and 1 (
changeset pre exitclose-out) can only run after RC exit. Nothing is left untested for lack of effort.Four more defects this round, on top of the nine already filed.
🔴 #4477 — screen-flow
resumeperforms no server-side validationA two-field screen node (
kindrequired;escalation_reasonrequired +visibleWhen: "kind == 'escalate'"):resumed with expected actual {"kind":"normal"}— conditional field hiddenaccept 200 ✅ {"kind":"escalate"}— field VISIBLE, required, missingreject 200 ❌ {}—kinditself required, missingreject 200 ❌ {"kind":"normal","totally_bogus":"x"}— undeclared keyreject 200 ❌ The render half is fine — the screen payload carries
visibleWhenintact. But "honored in validation" passes only vacuously: nothing validates, so the conditional case cannot be got wrong. A client that posts straight toresumebypasses everyrequiredthe author declared. This is the one gap in an otherwise consistent row — action params (#4213), record writes (ADR-0113), and approvaldecisionOutputs(#3447) all enforce their declared contracts, verified working.#4475 — every Setup System Overview tile reads 0
Users 0 (actually 4), Sessions 0 (actually 16), all event counts 0. The widget's own response gives the cause:
{"rows":[{"user_count":0}], "sql":"SELECT COUNT(*) AS \"user_count\" FROM \"sys_user\" WHERE created_at = $1"}An "all time" range is lowered to
WHERE created_at = $1— an equality test on a timestamp. The cube path lowers ranges correctly (>= $1 AND < $2), so this is specific to the dataset path. First screen an operator sees, every number wrong, no error surfaced.#4476 — docs date the #4286 removals to spec 18; they ship in 17
Nine passages across three hand-written pages say
query.cursor/distinct/joins/windowFunctionswere "removed in@objectstack/spec18".PROTOCOL_VERSION = '17.0.0', the package is17.0.0-rc.1, andos migrate meta --from 16labels all four[protocol 17]. A reader on 16 is told the break is a major away and plans accordingly. Found by the sweep run the tracker asks for — the sweep's fingerprint list should gain a version-number check, not just surface names.#4478 — quorum progress and per-group chips do not render
The server publishes
behavior:"quorum",minApprovals:2, per-approvergrouplabels, and even a pre-rendered__nodeLabel: "Committee Sign-off (2 of 3)". The Console renders none of it: scraping the approval panel gives{lockBadge: true, approveBtn: true, quorumHint: false, groupChips: false}. An approver on a quorum node cannot see how many more approvals are needed. ThelockRecordhalf of the same line does render (审批中已锁定 badge), so the panel is otherwise correct — this is specifically the progress surface. Fix belongs in objectui #2811; no framework change needed, the data is already on the wire.Verified this round
- Sharing vocabulary (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).
groupandguestrecipients rejected with the legal list;team/business_unitaccepted;accessLevel:'full'stored as'edit'. Note: the spec rejectstype:'owner'(pinned by test) but the REST rule route silently dropstypeand any other unknown key — inconsistent with the 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 posture the data routes apply (a permission-set write rejectsactivewith a prescriptive message). Worth a follow-up. - better-auth 1.7.
sys_accountcarries a distinctissuercolumn, backfilled (local:credential), andid(row) is separate fromaccount_id— the precondition for unlink-by-row-id. Password sign-in worked throughout all four runs. droppedFields(审批场景下记录可写性的反馈全线失真:能改的显示「已锁定」,改不了的提示「更新成功」 #3794) — found it. The trigger is areadonlyfield, not a system or computed one:Both channels carry it. This closes the open question from runs 2–3 — the contract does have live coverage; computed fields 400 and system fields are a different path (PATCH /data/showcase_contact/<id> {"lead_score":999} → x-objectstack-dropped-fields: lead_score;reason=readonly → body: {"droppedFields":[{"object":"showcase_contact","fields":["lead_score"],"reason":"readonly"}]}created_atbeing writable is data: created_at is client-writable on an ordinary PATCH — the audit anchor can be forged silently #4447).- 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/Identity import: raw SQL leaks into row errors; DB constraint failures need sanitized, friendly messages #3566). A row with
created_at: "2020-03-15"andstatus: "done"imports with the timestamp preserved and the state machine skipped. Dry-run row errors are human-readable and located:{"row":2,"field":"status","code":"invalid_option","error":"Status: \"NOT_A_STATUS\" is not a known option"}— no raw SQL. FormSection.pane(feat(spec):FormSection.pane— explicit split-pane placement (objectui#2153 follow-up) #4160) —pane: "primary"/"secondary"reach the client in the view metadata.- AI family (B4) —
/ai/nlq,/ai/suggest,/ai/insightsanswer 501 (honest empty slot), not 404 and not a fabricated response;/ai/agentsserves the real surface. - ADR-0104 D3 wave 2 (feat(storage): 开启回收 — 释放的字段文件按部署门禁进入墓碑与回收 — ADR-0104 D3 wave 2 (PR-5b) (#3459) #4208) — on an unmigrated deployment an invalid media value is warn-first and says so ("accepted for now … run
os migrate files-to-references --applyto migrate this deployment and enforce"), collection stays off. The strictFileReferenceIdValueSchemademonstrably works — it caught the showcase's own data-URI cover. - Docs sweep run 2 — every retired-surface fingerprint hit outside
releases/andreferences/documents the removal correctly; the only drift is the version number in docs: nine passages date the #4286 query-surface removals to spec 18 — they ship in 17 #4476.
One open judgement call for the maintainers
/graphqlcorrectly 404s, but discovery still reports agraphqlservice slot:{"enabled":false,"status":"unavailable","message":"No implementation ships for the 'graphql' slot — register a service under it to enable"}. That message invites someone to fill a slot for a surface v17 deliberately removed, and it contradicts the docs sweep's own run-1 fix (services-checklist reduced to 16 services with every graphql row deleted). Left unticked: whether to drop the slot fromCORE_SERVICE_PROVIDERSor reword the remedy is a design call, not a test result.All 13 defects from this verification
Security / correctness: #4467 (analytics ignores record scoping — reads values a caller cannot read), #4433 + #4434 (a sharing rule's grants cannot be withdrawn and the rule cannot be deleted), #4447 (
created_atforgeable), #4441 (dangling lookup references incl. RBAC link tables), #4462 (unreachable datasource silently uses the default store), #4477 (screen resume unvalidated), #4466 (admin override unrecorded).Contract / envelope: #4431 (capability denial 400 vs the 500 the crash contract promises), #4435 (missing-record writes report success), #4436 (filter-operator refusal has no
error.code), #4437 (analytics measure 500s withSQLITE_ERROR), #4432 (meta overlay type-segment phantom rows), #4455 (value-shapes gate misses its own headline case).UI / docs: #4475 (Setup overview all zeros), #4478 (quorum chips), #4476 (docs version drift).
- Sharing vocabulary (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).
All defects from this verification are now rolled up in #4482 — 17 issues grouped as security/data-integrity (8), contract/envelope (6), UI/docs (3), with a suggested RC-exit triage and the two non-defect judgement calls (
/graphqldiscovery slot; two stale checklist claims).Note a correction to my run-4 comment above: it said "13 defects". The grouped list was right, the total was a miscount — it is 17.
rc.3 verification — G1 regression + G2–G10 first pass
Environment:
main@a1a855a28(the rc.3 train tip — note rc.3's version commitc6a52d3a4was tagged and published to the npmrcdist-tag but not merged back, so main'spackage.jsonstill reads17.0.0-rc.2while main sits 56 commits past the rc.3 cut). Real showcase boot, two live principals, purpose-built probe stacks for the rest.73 of 113 ticked. Two new defects.
G1 — 16 of 17 fixes verified landed, 1 residual
I re-ran every original repro rather than trusting the close. The fixes are real and several are better than what I asked for:
- analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467 (the aggregate-disclosure one) — member with shares on 2 of 5 notes: data 2, analytics 2, and
dimensions:["title"]returns only the two titles they may read. - datasource: an object mapped to an unreachable datasource silently reads/writes the DEFAULT store — no boot failure, /ready still 200 #4462 — boot now fails, naming the datasource, naming the object routed to it, stating the consequence ("their reads/writes would otherwise land in a DIFFERENT database than the one they declare ⇒ fail-fast per ADR-0062 D5"), and offering
OS_ALLOW_DRIVER_CONNECT_FAILURE=1. - 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 #4434 — deactivating a rule withdraws immediately (200 → 404); DELETE returns 204, not 500.
- screen flows: resume performs no server-side validation — missing required fields and undeclared keys all complete the run #4477 — resume validates: missing required 400, undeclared key 400, valid 200 and the write lands. (Method note: the envelope is
{"inputs":{…}}. I first tested flat and got a false failure — the docs sayinputs, I was wrong.) - approvals: an admin override of a staffed approver slate is recorded as an ordinary approval — no override marker anywhere #4466 —
via_override: trueis written on the override decision,nullon the ordinary one. - actions: sandbox capability denial answers 400 as a deliberate rejection, not the 500 the crash contract promises #4431 capability denial → 500
INTERNAL_ERROR, noSandboxError:prefix. data: created_at is client-writable on an ordinary PATCH — the audit anchor can be forged silently #4447created_atunchanged. data: a lookup accepts an id that does not exist in the referenced object — including the RBAC permission-set link tables #4441 dangling lookup 400. data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435 PATCH/DELETE missing → 404. data: unsupported-filter-operator refusal ships without error.code and leaks the [sql-driver] prefix #4436INVALID_FILTER, no driver prefix. analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437 measure → 400. migrate value-shapes: a lookup holding an embedded record object passes the scan — the exact case the ADR-0104 D1 gate exists to find #4455 scan reports the embedded reference with a correct-id example.
Residual → #5079. #4432's namespace normalisation, shadowing and immediate dispatch are all fixed, but after DELETE the overlay stays in the
/metalisting while dispatch correctly 404s. The delete is real (reset:true, nosys_metadatarow); only the enumeration view is stale — the mirror of the seam #4521 fixed on the write side.G2–G10 — what passed
- ADR-0118 —
sys_metadata_history.recorded_byis NULL on every row, no'system'sentinel. Boot banner readsTenancy: single(resolved posture, not the legacy boolean). - ADR-0119 atomic batch, proven by rollback. An atomic
deleteManyof[valid, nonexistent]: the valid row comes backROLLED_BACK, the missing oneRECORD_NOT_FOUND, and the valid row still exists afterwards (GET 200). Per-row results carry the declaredBatchOperationResultSchemashape withindex+errors[], and the newNOT_ATTEMPTEDcode appears on rows after an abort. - G4 — the semantic reversal landed. An unevaluable hook condition now aborts:
400 Hook 'x' could not evaluate its condition (runtime: No such key: …) — operation aborted, naming the field and giving two remedies. And the root cause is gone: the rc.1 boot logged dozens ofcondition evaluation failed; treating as false — No such key: done; on rc.3 that count is 0 (bug(objectql): hook 条件对着「只含本次更新字段」的残缺 record 求值,取不到的字段被吞成 false —— 审计 hook 静默不触发 #4770 + bug(objectql/docs): hookcondition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784). - G6 — a
env: ['prod']seed loads 0 rows in dev (fix(seed): enforce Seed.env against the runtime environment #4836);NOW()resolves per field type (datetime → full instant, date → calendar day, 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); a fresh DB reports "Physical schema is in sync — nothing to migrate" instead of the phantom drift rc.1 showed ([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). - G7/G8 — the retirement diagnostics are the best I have seen in this codebase.
areas[].visibledoesn't just say "removed": it explains the key was a gate that failed open, which layer to move to, and that server-enforced gating requires the top-level tree because "the server does not walkareas".datasource.retryPolicywarns against renaming your way around it becausehook.retryPolicyis a genuinely-enforced different key.composeStacks(composeStackssilently drops every non-array top-level key —api:today,server:as of #4910 #5005) is three-for-three: undeclared key warns, conflict throws naming both stacks, same value passes. - G10 docs sweep run 3 — every fingerprint hit for this window's retired keys is either documenting the removal or a same-name-different-thing (a plugin's
healthCheck()method,responsiveStyles). Clean.
New defect → #5086
allowRuntimeCreate: falseis not enforced. #4509 set the flag onjoband its changeset says "no 'create job' in Studio or viaPUT /meta". It still creates one:PUT /api/v1/meta/job/rc3_runtime_job {"schedule":{"type":"cron","expression":"0 0 * * *"},"handler":"nope"} → 200 "Saved customization overlay (env-wide) — type=job" … and it lists, with handler "nope"agent, the only other flagged type, behaves the same.supportsOverlay: falselooks equally unenforced — the success message calls the row a "customization overlay" for a type declared not to support them.This is the ADR-0049 failure mode one level up: the enforcement flag itself is the silently-inert declaration. It is easy to miss because a minimal payload 422s on schema validation first — only a payload that passes the schema reveals that the gate never runs. Studio presumably reads the flag to hide "create", so the UI honours a rule the API does not.
Remaining 40
Mostly G5 (metadata-plane honesty:
publishDraftdrain, DDL loudness, history/event_seq invention), G9 (client-react hook loops — needs a React harness), the G8 spec-rename family (compile-time, best covered by a typecheck of a consumer rather than a runtime probe), and the close-out item that can only run atchangeset pre exit.- analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467 (the aggregate-disclosure one) — member with shares on 2 of 5 notes: data 2, analytics 2, and
rc.3 verification — complete
108 of 113 verified. The 5 open items are: 2 blocked on defects filed from this pass (#5086, #5088), 2 close-out items that can only run after
changeset pre exit, and the #4432 residual tracked in #5079.Three new defects this round, on top of the seventeen from the A–F passes (all of which are now closed and re-verified as genuinely fixed, not merely closed).
New defects
# What #5086 allowRuntimeCreate: falseis not enforced.PUT /metacreatesjobandagentitems the registry declares code-only.supportsOverlay: falselooks equally unenforced — the success message calls the row a "customization overlay" for a type declared not to support them. This is the ADR-0049 failure mode one level up: the enforcement flag itself is the silently-inert declaration.#5088 updateManyruns hooks for a nonexistent id. With no stored record to merge, the hook condition evaluates against a payload-only record, #4775's abort fires, and the row failsINTERNAL_ERRORaccusing a correct hook of naming an undeclared field — where single-record PATCH correctly answersRECORD_NOT_FOUND. Underatomicone stale id poisons the whole batch viaNOT_ATTEMPTED.#5079 #4432 residual — a deleted overlay stays in the /metalisting while dispatch correctly 404s.The seventeen earlier fixes: verified, not assumed
I re-ran every original repro. All landed. Several exceed what was asked:
- datasource: an object mapped to an unreachable datasource silently reads/writes the DEFAULT store — no boot failure, /ready still 200 #4462 now fails the boot naming the datasource, naming the object routed to it, stating the consequence ("their reads/writes would otherwise land in a DIFFERENT database"), and offering an escape hatch.
- analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467 — member with shares on 2 of 5 notes: data 2, analytics 2,
dimensions:["title"]returns only their two titles. - screen flows: resume performs no server-side validation — missing required fields and undeclared keys all complete the run #4477 — resume validates three ways and the write lands. (The envelope is
{"inputs":{…}}; I first tested flat and produced a false failure. The docs were right.) - Console approvals: quorum progress and per-group chips do not render, though the server sends minApprovals / group / __nodeLabel #4478 — the quorum UI now renders 会签 — 已完成 0 / 共 2 个组 with a progress bar and per-group chips
finance 0/1,manager 0/1. - Setup System Overview: every KPI tile reads 0 — the dataset path lowers an all-time range to WHERE created_at = $1 #4475 — Setup System Overview reads Users 4, Sessions 7. Real counts.
Highlights from the rc.2/rc.3 window
- ADR-0119 atomicity proven by rollback, not by wording: an atomic
deleteManyof[valid, missing]marks the valid rowROLLED_BACKand the row still exists afterwards. - The G4 semantic reversal is real and its root cause is gone. An unevaluable hook condition now aborts with a prescriptive 400; the
condition evaluation failed; treating as falsewarnings that flooded the rc.1 boot log are down to 0. The same fail-closed posture holds for validation predicates (Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649). - Parent-scoped
readonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889 parent-scopedreadonlyWhenis enforced server-side — on a paid invoice bothproductandunit_pricecome back indroppedFieldswithreason=readonly_whenand the stored values are unchanged, while the same fields on a draft parent write normally. (My first attempt passed vacuously because I re-sent the existing value — worth stating for whoever re-tests.) - 审批记录锁
bindApprovalLockHook对谓词式(multi)更新完全失效:if (!id) return把「没解析到行」当成「允许」 #4778 the record lock holds on the predicate/multi path (RECORD_LOCKEDper row), not just single PATCH. - The retirement diagnostics are the strongest work in this window.
areas[].visibleexplains it was a gate that failed open and which layer actually enforces;datasource.retryPolicywarns against renaming your way around it becausehook.retryPolicyis a genuinely-enforced different key;composeStacksis three-for-three (undeclared warns, conflict throws naming both stacks, same value passes). - G8 spec convergence — 15 renamed exports checked across 10 subpaths: no duplicate owners. G10 docs sweep run 3 — every fingerprint hit is documenting a removal or a same-name-different-thing; clean.
One operational note, not a defect
On this build
/api/v1/meta/objecttakes ~11s (680 KB) and/meta/view~9s. The Console blocks its splash on both, so a cold/_console/load sits on "Connecting to data source" for ~20s before rendering. It does render correctly. Worth a look before GA if that is not expected for a 24-object app.
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.