Repository navigation
feat(platform-objects): sys_personal_view, a reader's own named list views, with the owner's delete door (server step) - #29575
Conversation
…views (server half, first version) A user-owned platform object on the sys_user_preference template: owner, object name, display name, a ListView body validated on every save, and an is_default flag kept unique per owner and object. Owner-only row scope; the organization admin's wildcard bypass is closed for this object. Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
… save check and one default Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…in's recorded boundary Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…birth, outside the ruling's 23 Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…ys_personal_view The extract config now imports SysPersonalView, so the en bundle carries it; the guard's own instruction is to add the object to its ownership list. Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…by its packageId The fake engine kept the LAST afterInsert handler registered, which on this boot is now sys_personal_view's default rule (registered after the stamp on kernel:ready). Select the stamp by the packageId it registers under instead. Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…personal_view sys_personal_view joins the population in the machinery's reach (its organization column), 92 registered / 47 in reach. Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…E /api/v1/personal-views/:id A member removes their own sys_personal_view row, and only theirs, with no object-level delete grant (seat ruling 6583289013; the #26161 unreact-door shape). The row is read as the caller (another member and an organization administrator get the unknown-id 404), a readable row the caller does not own is refused 403 PERMISSION_DENIED, and only then is it removed as the system, attributed to the caller inside the caller's organization. member_default is unchanged and the generic data DELETE stays refused. The route has its ledger row (personal-view-route-ledger.ts + conformance), its route-envelope declaration and its single-authz-resolver delegator row. Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…w door by name; tenant audit census counts its write The door's removal is a new elevated write call site (256). The census artefacts are regenerated and the page's hand-written figures follow it. Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…r's ledger The door mounts on the IHttpServer port, so the parity gate's union of ledgers needs its ledger; the dogfood test task declares the imported source as an input (cross-package-test-inputs + turbo.json). Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 98 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 16 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 182 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 061d34e2bd151348b0277ed934f6b5968fa3ad3c && git checkout 061d34e2bd151348b0277ed934f6b5968fa3ad3c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 81cd9eb0e9f2fb32a9ae01ef60dc4da45f892ef2 deedb538e508cb01955a0a03192fa39a457a3ddb && git checkout -B drift-repro 81cd9eb0e9f2fb32a9ae01ef60dc4da45f892ef2 && git merge --no-ff deedb538e508cb01955a0a03192fa39a457a3ddb
node scripts/docs-audit/affected-docs.mjs --json 81cd9eb0e9f2fb32a9ae01ef60dc4da45f892ef2
|
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract-review-claim: session_01ADHkmQjPiXgNoSaEpqeGvY · head 805f048 · 2026-11-02T19:31Z |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract reviewServed-tier: Read: card #29443 (body and all 5 comments, including seat ruling 6583289013 and dev report 6586486609), parent ruling 6570568131 on #28971, the PR body, its 32-file list, the net diff, and the check-runs on this head. The cited code was read on ① Derived judgmentsAccept-set and public-surface changes the diff implies
Security of the door
The four forced test edits are legitimate. None loosens a guard.
Check-runs on this head There are 65 runs: 58 success, 7 skipped, and none failed or pending.
Must-fix
Should-fix
Nits
② Semver level
③ Boundary flagsDev's open question: should Dev flags (deviations)
Escalated
Implemented-by: VERDICT: FAIL Stamped 2026-11-02T19:53Z. |
…e listed object Round-2 review fixes for sys_personal_view: - Remove the is_default column and its one-default hook. The default view is the Console's existing per-object pin, which holds any view id. - A saved ListView whose data.object names another object than the row's object_name is refused 400 VALIDATION_FAILED on config.data.object. - Pin that the owner cannot hand a view to another user (403), and record that admin_full_access reads, edits and deletes through the data door. - external_default gets no grant in this version. - Correct the object comment: a wildcard carrying viewAll/modifyAll still reaches a private object. Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
… afterInsert remains Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
For an organization member, member_default's '*' update floor (created_by == current_user.id) is a held update policy, so the object's declared defaultRowScope does not apply to an update, and a re-parent's post-image passes it. The beforeUpdate rule now refuses a non-system update whose user_id differs from the stored owner (403 PERMISSION_DENIED). Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
…ner rule Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
nameField 'label' alone names the title (#29007 retired titleFormat on code-shipped objects; it arrived with the merge from main). Claude-Session: https://claude.ai/code/session_01ADHkmNjCFNPZLkGxdFdgAq Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract-review-claim: session_01ADHkmQjPiXgNoSaEpqeGvY · head deedb53 · 2026-11-02T22:23Z |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract reviewServed-tier: Round 2 of the review on #29575, per seat ruling 6587007982 point 8: the spec roster path at this tier, and the rest of the 32-file diff judged in this same record as the lane seat's ACCEPT at ① Derived judgmentsRound-1 findings and ruling points on this head
Accept-set and public-surface changes the diff implies
Check-runs on this head: 69 runs, all completed; 60 ② Semver level
③ Boundary flagsDev open question 1, platform administrator's reach: answered by ruling 3, option A; kept as a pinned recorded boundary and decided with Dev open question 2, external members: answered by ruling 5; out of scope here, the ui half's question. Dev deviations (round-2 report)
Dev out-of-scope finding: Escalated: nothing. Both round-1 escalations (the tier and the spec allowance) are closed by ruling points 8 and 1. ④ Design judgment
The rest of the diff at
Must-fix: none. Should-fix
Neither should-fix changes behaviour or the accept set, and neither is a reason to hold the PR; both can land with S3's follow-up or in the ui half's PR if the seat prefers. Implemented-by: VERDICT: PASS Stamped 2026-11-02T22:33Z. |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #29443. 32 files (+1931/−34): sys_personal_view in platform-objects, its hooks and the own-row delete door DELETE /api/v1/personal-views/:id in plugin-auth (ledger, conformance, route-envelope, authz-resolver rows), the organization-admin per-object entries in plugin-security, the roster, census and i18n edits, tests, a dogfood pin, and one patch changeset (platform-objects, plugin-auth, plugin-security, spec, cli). The only packages/spec/** path is the PLATFORM_OBJECTS_BY_PACKAGE line ruled allowed. Not governed (check-governed-merges governed: false). |
| Rule | A reader saves, renames and deletes their own named list views. The row scope is owner-only and organization-scoped. Organization admins are closed out by per-object entries, and the door's 404/403 order leaks no row. admin_full_access keeps read, edit and delete through the generic data door, as a pinned and stated boundary that matches sys_user_preference. A save rule refuses any non-system user_id change with 403, closing the re-parent path the dev found (member_default's '*' update floor replaces the object's scope on UPDATE). A config.data.object that differs from object_name is refused 400. No is_default: the Console pin #27594 holds a personal-view id as it is. External members get no grant in this version. |
| Rulings | Ruling 6587007982 holds, and the route name is accepted. Open questions carried to the ui half: listing personal views in the switcher, and the external posture. |
| Not in this PR | Two comment-only should-fixes of 6590198210 (census row 26c and the hooks docblock should name system-elevated automation as what the isSystem skip admits; a stale "default flag" comment in plugin-auth/src/manifest.ts) ride the S3 follow-up card. The reviewer confirmed, from main, that sys_user_preference has the same re-parent gap; the seat files that as its own card. |
| Evidence | Engine, door, ledger and dogfood REST suites green. Ablations 1–9 each red, then restored. Gates 133/133, --ran 0 not measured. Battery: 399 files, 3296 passed, 0 failed. CI on this head: 69 runs, 60 green, 9 skipped, none red. check-expected-skips OK (9, all in main's roster). |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay. |
Census files re-derived on the merge: platform-object-tenancy-census and tenant-audit-census (261 write call sites with sys_personal_view and sys_user_device both counted), with the census page's hand-written figures moved to match. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmRD9KKqvTU3THnute6
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Requested by Steve · project thread
Fixes #29443
Clause-②: yes (widening)
Metadata-protocol: none
Clause-② measured: on base (
origin/main@81cd9eb0),sys_personal_viewdoes not exist, so every/api/v1/data/sys_personal_viewcall is refused as an unknown object. No registrar mountsDELETE /api/v1/personal-views/:idthere (read from the source; not driven over HTTP on base). On head both are served, and the dogfood file below is 16/16. No existing answer narrows: the only grants added name the new object, no shipped set loses anything, and the generic dataDELETEkeeps answering what it answered (pinned). Metadata-protocol staysnone. This is a platform object holding user data, not a key, value or metadata type an author writes, and/metadoes not serve it (pinned).This closes the server step of #29443: read, create, rename, and the owner's own delete. Which view opens by default stays with the Console's existing per-object pin (see Round 2). The Console half follows on the card.
维护者速读
sys_personal_view(个人视图)。一行就是某个用户为某个对象保存的一个命名列表视图,含所有者、对象名、名称和 ListView 配置。通用数据 API 可以读、建、改名。默认视图不在这里存,沿用 Console 已有的按对象置顶(list.view.pinned,Console lists: the list-view switcher becomes a dropdown on the list title (search, pinned default, recent), the flat view-tab row retires, and a starred view appears in the sidebar's 收藏 group #27594)。另外新增一个所有者删除门DELETE /api/v1/personal-views/:id,只能删除调用者自己的视图。sys_user_preference为模板。删除门按席位裁决 6583289013 建,沿用 security: a member with no position can add a comment reaction but never remove it — the console removes by delete, and delete may not ride an everyone-anchored set #26161 字母 A 的形状。admin_full_access)保留通配:通过通用数据 API 能读、改、删任何成员的视图,和sys_user_preference的现状一致,已有测试钉住,并列为待决问题;只有本 PR 的删除门拒绝他删别人的视图(403)。外部成员本版没有授权,也列为待决问题。视图的所有者在保存时确定,任何非系统调用都不能改user_id(403)。回滚就是 revert 本 PR,表留着无害。packages/platform-objects/src/identity/sys-personal-view.object.ts;packages/plugins/plugin-auth/src/personal-view-hooks.ts;packages/plugins/plugin-auth/src/personal-view-routes.ts;packages/plugins/plugin-security/src/objects/default-permission-sets.ts里的两处新条目。pnpm --filter @objectstack/plugin-auth exec vitest run src/personal-view-hooks.test.ts src/personal-view-routes.test.ts,预期 35 个全绿;pnpm --filter @objectstack/dogfood exec vitest run test/personal-view-row-scope.dogfood.test.ts,预期 16 个全绿。/api/v1/personal-views/:id已由席位接受;两个待决问题(平台管理员的通配、外部成员)见文末。What changes
New platform object
sys_personal_view(packages/platform-objects/src/identity/sys-personal-view.object.ts). plugin-auth's identity manifest registers it right aftersys_user_preference, on the same template.user_id(owner, lookupsys_user),object_name,label(the name the owner gave it),config(json, spec'sListViewshape), andcreated_at/updated_at. There is no default column: which view opens by default is the Console's existing pin (see Round 2).managedBy: 'system-data'andaccess: { default: 'private' }, so a plain (non-super-user)'*'wildcard does not reach it. A wildcard carryingviewAllRecords/modifyAllRecordsstill does (ADR-0066 D2):organization_admin's is closed below, andadmin_full_access's is not.defaultRowScopeuser_id == current_user.idfor every operation.Two write rules (
packages/plugins/plugin-auth/src/personal-view-hooks.ts). Both registrars register them:AuthPluginonkernel:ready, andIdentityObjectsPlugin.start()on a kernel without auth.ListViewfor the row's own object. A create, and an update that sendsconfigorobject_name, parse the resulting body with spec'sListViewSchema; an update fills whichever of the two it did not send from the stored row. A body that does not parse is refused400 VALIDATION_FAILED(an existing code, throughvalidationFailure), with onefields[]entry per issue, each path underconfig. Text that is not JSON is refused the same way, with the single fieldconfig. A body whosedata.objectnames another object thanobject_nameis refused the same way, onconfig.data.object(codeobject_mismatch). A rename that sends neither is not re-judged.user_idis refused403 PERMISSION_DENIEDfor every caller but the system, platform administrators included; an echo of the stored owner is no change. The row scope alone does not hold this for an organization member:member_default's'*'update floor (created_by == current_user.id, positionsorg_member) is a held update policy, so the object's declared default does not apply to an update, and a re-parent's post-image still carries the member's owncreated_by. The round-2 battery found it (RESTPATCH user_idanswered 200).The owner's delete door,
DELETE /api/v1/personal-views/:id(packages/plugins/plugin-auth/src/personal-view-routes.ts). Seat ruling 6583289013; it takes the shape of security: a member with no position can add a comment reaction but never remove it — the console removes by delete, and delete may not ride an everyone-anchored set #26161's unreact door, ruling 6422861164 letter A.AuthPluginmounts it on the kernel's HTTP server, next to its auth routes. It removes one row, and only the caller's own:resolveRequestExecutionContext.401 UNAUTHENTICATED.403.400 VALIDATION_ERROR.404 RECORD_NOT_FOUND.user_id !== caller; today only a platform administrator reads one) is refused loudly with403 PERMISSION_DENIED, and the row stays. This is the door's own answer; the platform administrator's generic dataDELETEis not refused (see Grants).attributedUserIdset to the caller and inside the caller's organization. The answer is200 { id, deleted: true }.No grant moves:
member_defaultis unchanged, and the generic dataDELETEof the owner's own row is still403(pinned). Route name:/api/v1/personal-views/:id, the unreact door's convention (a plural resource noun under/api/v1, the record id in the path, no query). The seat accepted it in review.packages/plugins/plugin-auth/src/personal-view-route-ledger.ts(gap: the Console's My views list is the consumer, and the SDK has no method yet), withpersonal-view-route-ledger.conformance.test.ts.scripts/check-route-envelope.mjsdeclaration (responses: 0, ok: 0, err: 0; every body goes through the shared pair).scripts/check-single-authz-resolver.mjsdelegator row.node scripts/tenant-audit-census.mjs --writeregenerated the artefacts (256 sites), and the hand-written figures oncontent/docs/permissions/tenant-audit-census.mdxfollow it (255 → 256, 78 → 79, 145 → 146).DELETEverb (a partial embedding, or a test double) skips the door with a warning that names it, instead of failing the boot.Grants (
packages/plugins/plugin-security/src/objects/default-permission-sets.ts):member_default: read, create and edit onsys_personal_view, and no delete. The set is bound to theeveryoneanchor, which may not carry a delete bit (ADR-0090 D5).organization_admin(and the derivedorganization_admin_no_bypass): an explicitsys_personal_viewentry with noviewAllRecords/modifyAllRecords. For this object it overrides the wildcard's super-user bits, thesys_audit_logpattern. An organization administrator reads, writes and deletes only their own views.admin_full_accessis untouched (seat ruling 6587007982). Its wildcard carriesviewAllRecordsandmodifyAllRecords, so through the generic data API it reads, edits and deletes every member's view, exactly as it doessys_user_preferencetoday. Its pins forbid a per-object entry there, and the rescue path depends on it. Pinned as a recorded boundary at engine and REST level; listed as an open question below. Only the owner's door refuses it (403).external_defaultgets no grant in this version: an external member cannot save a personal view. Listed as an open question for the Console half.The id is the engine's record id (nanoid). It never contains
., so it can never be read asobject.keyview syntax (pinned).Not view metadata:
/meta/viewdoes not list a personal view, and/meta/view/<id>answers 404 (pinned). There is no metadata type, no package export and no metadata write door.The only spec touch (allowed by seat ruling, recorded in round 2): one line,
'sys_personal_view'inPLATFORM_OBJECTS_BY_PACKAGE['platform-objects'](packages/spec/src/system/constants/platform-object-names.ts). Nothing else underpackages/specchanges.Rosters:
fate: 'attribution');scripts/platform-object-tenancy-census.json, regenerated (92 registered / 47 in reach).content/docs/permissions/system-context.mdx: row 26c owns the owner rule's system skip (ctx.session.isSystem), and the generated site register is rewritten with--write(166 read sites).Changeset
.changeset/29443-personal-views-server.md: patch for platform-objects, plugin-auth, plugin-security, cli and spec (the spec package publishes a changed constant, and AGENTS.md asks every publishing change to carry a changeset).Not in scope, and not touched: sharing, team views, admin management, a field picker, packaging, any
ui/**change, #29037'sViewScopeSchema.personal/sharing.type, and a shared own-row door helper (review S3; the seat files it as a follow-up card).Round 2 (review issuecomment-6586971553, seat ruling issuecomment-6587007982)
is_defaultcolumn and the one-default hook are gone. The default is the Console's existing pinLIST_PINNED_VIEW_PREFERENCE(list.view.pinned, Console lists: the list-view switcher becomes a dropdown on the list title (search, pinned default, recent), the flat view-tab row retires, and a starred view appears in the sidebar's 收藏 group #27594,ui/packages/app-shell/src/utils/workspacePreference.ts). Finding: the pin can hold a personal view's id as it is. It stores{"view": <id>, "v": 1}for any non-empty string, andObjectViewresolves it withresolveViewId(pinned, views.map(v => v.id), objectName), whose first step is an exact id match. So it works once the Console half lists personal views inviews. A nanoid has no., so the bare-key qualification step (<object>.<key>) never turns it into a metadata view's id. No second store.admin_full_accesskeeps its wildcard. Its generic GET, PATCH and DELETE of a member's view are pinned as a recorded boundary (engine and REST). The body, changeset and open question no longer say its delete is refused; only the door refuses it.viewAllRecords/modifyAllRecordsstill reaches a private object (ADR-0066 D2).external_defaultgets no grant. Open question for the Console half.user_idis refused403 PERMISSION_DENIEDand the owner is kept (pinned, engine and REST; ablation 9). Pinning it over REST found that the row scope did not hold it for an organization member (see Two write rules), so a save rule holds it now. The samecreated_byfloor applies tosys_user_preference: by reading the source, itsuser_idis not readonly and no hook guards it, so an organization member can likely re-parent a preference row too. That was not driven over REST, and it is reported as an out-of-scope finding, not changed here. Aconfig.data.objectthat differs fromobject_nameis refused400 VALIDATION_FAILEDonconfig.data.object, on create and on update (pinned, engine and REST; ablation 8).durability-swallow-repair.test.tsis reverted: with no personal-viewafterInsertleft, the SCIM-stamp fake passes unmodified (22/22).titleFormaton code-shipped objects;sys_personal_viewnow names its title bynameField: 'label'alone (the sweep inmanaged-api-method-affordance-sweep.test.tscaught it).Open questions
admin_full_accessreads, edits and deletes every member's personal view, exactly as it doessys_user_preference(pinned as a recorded boundary). Options: A keep it, and decide it withsys_user_preferenceas one question; B close it for both user-owned objects with a narrow engine-level exception, which needs the set's pins amended. Recommendation: A for this card, because the rescue path depends on the wildcard and the question is the same for both objects.external_defaultgrants nothing onsys_personal_view, so an external member cannot save a view in this version. Whether they should is a question for the Console half.Why
#28971 ruling C (comment 6570568131): a reader's named list views are user data, a user-owned platform object modelled on
sys_user_preference, not a view metadata scope. Seat ruling 6583289013 put the owner's delete in this step. This PR is that server step; the Console half follows on the card.Tests
packages/plugins/plugin-auth/src/personal-view-hooks.test.ts: 27 tests on real ObjectQL + SQLiteSqlDriver+SecurityPlugin... An update handing a view to another user is refused 403, and the owner is kept; a platform administrator, whom no row scope binds, is refused by the save rule alone; an echo of the stored owner is accepted (control).findOnereturns null,findreturns [],countis 0, and an update answers404 RECORD_NOT_FOUND. A create naming another owner is refused403 PERMISSION_DENIED.organization_adminandorganization_admin_no_bypassboth see none of a member's views (404). An organization admin keeps their own views (control). The recorded boundary:admin_full_accessreads, edits and deletes a member's view through the engine.configis refusedVALIDATION_FAILED, with paths underconfig, on create and on update, and the stored body is unchanged. Also covered: non-JSON text, and a rename withoutconfig.config.data.objectis another object, an update moving onlyobject_name, and an update sending only a body for another object are each refused onconfig.data.object, with the row unchanged. Control: changing both together is accepted.removeOwnPersonalView): the owner's row is removed and the owner's other views stay. Another member gets 404 and the row stays. Both organization-admin sets get 404 and the row stays. The platform admin gets 403 and the row stays. An unknown id gets 404. The generic engine delete of the owner's own row is still 403.packages/plugins/plugin-auth/src/personal-view-routes.test.ts: 8 tests on the door's shell.DELETE /api/v1/personal-views/:id.{ isSystem, attributedUserId: caller, tenantId: caller's org }.packages/plugins/plugin-auth/src/personal-view-route-ledger.conformance.test.ts: 3 tests. Every mounted route has a ledger row, and every row is mounted.packages/qa/dogfood/test/personal-view-row-scope.dogfood.test.ts: 16 tests through REST, on the showcase app booted with its default security.RECORD_NOT_FOUND, and the generic delete answers403 PERMISSION_DENIED. A planted row naming another owner gets 403.sys_user_permission_set): 404.400 VALIDATION_FAILED; a body listing another object:400onconfig.data.object.showcase_project.budgetcan save a view filtering onbudget, because a save does not re-judge field security. The list queryGET /data/showcase_project?$filter={"budget":{"$gt":0}}is then refused 403, namingbudget./meta/viewdoes not serve it.[door]: the owner gets 200 and the row is gone. Another member gets 404 and the row stays. An organization admin gets 404 and the row stays. The platform admin's door delete gets 403, and the row stays. Recorded boundary: the platform admin's generic GET, PATCH and DELETE of a member's view succeed. Anonymous gets 401. The genericDELETE /data/sys_personal_view/<own id>is still 403.packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts: the census of objects declaringdefaultRowScopegainsDECLARED_AT_BIRTH = { sys_personal_view }. It is kept separate from the ruling's 23DECLARERS, which are unchanged. A newit.eachasserts the literal and that no shipped set carries a copy.packages/platform-objects/src/apps/translations/bundle-ownership.test.ts:OWNED_OBJECTSnow namessys_personal_view. That is the guard's own instruction when the extract config gains an object.packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts: the gate's union of ledgers readsPERSONAL_VIEW_ROUTE_LEDGER, because the door mounts on theIHttpServerport. Without it, "every mounted route is ledgered" was red on the full battery:DELETE /api/v1/personal-views/:id. The dogfood test task declares the imported source as an input inscripts/cross-package-test-inputs.mjsandturbo.json, the same as the storage, i18n and settings ledgers.Ablations (each red, then restored,
git diff HEADempty)organization_adminentry forsys_personal_vieworganization_admintest (404 → row read).organization_admin_no_bypassstays green, as expected: that variant strips the bypass bits itself.defaultRowScope(platform-objectsdist/rebuilt, preflight confirmed absent)ListViewcheck (both hooks)config.data.objectthat differs fromobject_namethrough[owner] cannot hand their viewanswered 200dist/rebuilt)[predicate]test (200 instead of 403)row.user_id !== callerId)AuthPlugindoes not mount it (plugin-authdist/rebuilt, preflight confirmed the marker)[door]tests. The generic-delete control stays green, as expected.The
/metapin is structural, because no code path serves the object as metadata, so it has no mutation to ablate.Verification
Round 2, on head
deedb538(main81cd9eb0merged; no conflicts):os-verify-lock: specplatform-object-names.test.ts12/12; platform-objects 110 files, 1512 tests; plugin-auth 215 files, 3693 passed / 10 skipped; plugin-security 241 files, 5203 passed / 45 skipped; cliorganization-ownership-inventory.test.ts26/26. Typecheck exit 0 for platform-objects, plugin-auth, plugin-security and cli.pnpm lint(root eslint,--no-inline-config) on every changed file: exit 0.check-i18n-bundles: OK.check:system-context-census: OK (166 read sites).check:doc-anchors: OK.node scripts/pm/dispatch-gates.mjs --commandslisted 133 commands. All 133 exit 0 ondeedb538(check:type-check-debtrun underos-verify-lockafter the battery), reconciled with--ran: 133 derived, 133 run, 0 NOT-MEASURED.pnpm --filter @objectstack/dogfood testunderos-verify-lock,VERDICT command-exit 0 · held the lock 1583s(shared box), Test Files 399 passed / 3 skipped (402), Tests 3296 passed / 28 skipped (3324), no failures.check:type-check-debtafter it: exit 0.ee23b8eahad exactly one red,[owner] cannot hand their view(200, not 403). That was the re-parent gap the fixed-owner rule closes. CI on9f7adf09andd4773b47found the census row and the retiredtitleFormat; both are fixed on this head.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code