Skip to content

feat(platform-objects): sys_personal_view, a reader's own named list views, with the owner's delete door (server step) - #29575

Merged
objectstack-fleet[bot] merged 23 commits into
mainfrom
claude/project-thread-xtjloc
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 23 commits into
mainfrom
claude/project-thread-xtjloc

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

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_view does not exist, so every /api/v1/data/sys_personal_view call is refused as an unknown object. No registrar mounts DELETE /api/v1/personal-views/:id there (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 data DELETE keeps answering what it answered (pinned). Metadata-protocol stays none. This is a platform object holding user data, not a key, value or metadata type an author writes, and /meta does 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,只能删除调用者自己的视图。
  • 为什么改:依据 [Decision] Named personal list views: can a reader save their own named list, and what is the end state? #28971 裁决 C,「我的视图」是用户数据,不是视图元数据。这是第一版的服务端一半,以 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 的形状。
  • 风险与回滚:这是纯新增对象和新增路由,已有对象和已有答复都不变。行级范围只到所有者本人;组织管理员的通配绕过对该对象已关闭,读不到、改不了、也删不了成员的视图(都答 404)。平台管理员(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 after sys_user_preference, on the same template.

    • Fields: user_id (owner, lookup sys_user), object_name, label (the name the owner gave it), config (json, spec's ListView shape), and created_at / updated_at. There is no default column: which view opens by default is the Console's existing pin (see Round 2).
    • It is managedBy: 'system-data' and access: { default: 'private' }, so a plain (non-super-user) '*' wildcard does not reach it. A wildcard carrying viewAllRecords / modifyAllRecords still does (ADR-0066 D2): organization_admin's is closed below, and admin_full_access's is not.
    • It declares the owner-only defaultRowScope user_id == current_user.id for every operation.
    • The table carries the organization column, so the Layer 0 wall bounds it too (ADR-0131). The regenerated tenancy census counts it in reach.
  • Two write rules (packages/plugins/plugin-auth/src/personal-view-hooks.ts). Both registrars register them: AuthPlugin on kernel:ready, and IdentityObjectsPlugin.start() on a kernel without auth.

    • The body must be a ListView for the row's own object. A create, and an update that sends config or object_name, parse the resulting body with spec's ListViewSchema; an update fills whichever of the two it did not send from the stored row. A body that does not parse is refused 400 VALIDATION_FAILED (an existing code, through validationFailure), with one fields[] entry per issue, each path under config. Text that is not JSON is refused the same way, with the single field config. A body whose data.object names another object than object_name is refused the same way, on config.data.object (code object_mismatch). A rename that sends neither is not re-judged.
    • The owner is fixed when the view is saved. An update that changes user_id is refused 403 PERMISSION_DENIED for 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, positions org_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 own created_by. The round-2 battery found it (REST PATCH user_id answered 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. AuthPlugin mounts it on the kernel's HTTP server, next to its auth routes. It removes one row, and only the caller's own:

    1. The caller is resolved through the one composition, resolveRequestExecutionContext.
    2. Anonymous is refused 401 UNAUTHENTICATED.
    3. A held sign-in gets its policy's 403.
    4. A query parameter is refused 400 VALIDATION_ERROR.
    5. The row is read as the caller. Another member, an organization administrator, and an unknown id all get 404 RECORD_NOT_FOUND.
    6. A row the caller can read but does not own (user_id !== caller; today only a platform administrator reads one) is refused loudly with 403 PERMISSION_DENIED, and the row stays. This is the door's own answer; the platform administrator's generic data DELETE is not refused (see Grants).
    7. Only then is the row removed, as the system, with attributedUserId set to the caller and inside the caller's organization. The answer is 200 { id, deleted: true }.

    No grant moves: member_default is unchanged, and the generic data DELETE of the owner's own row is still 403 (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.

    • Its ledger row: 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), with personal-view-route-ledger.conformance.test.ts.
    • Its scripts/check-route-envelope.mjs declaration (responses: 0, ok: 0, err: 0; every body goes through the shared pair).
    • Its scripts/check-single-authz-resolver.mjs delegator row.
    • Its removal is one new elevated write call site in the tenant audit census. node scripts/tenant-audit-census.mjs --write regenerated the artefacts (256 sites), and the hand-written figures on content/docs/permissions/tenant-audit-census.mdx follow it (255 → 256, 78 → 79, 145 → 146).
    • A server that registers no DELETE verb (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 on sys_personal_view, and no delete. The set is bound to the everyone anchor, which may not carry a delete bit (ADR-0090 D5).
    • organization_admin (and the derived organization_admin_no_bypass): an explicit sys_personal_view entry with no viewAllRecords / modifyAllRecords. For this object it overrides the wildcard's super-user bits, the sys_audit_log pattern. An organization administrator reads, writes and deletes only their own views.
    • admin_full_access is untouched (seat ruling 6587007982). Its wildcard carries viewAllRecords and modifyAllRecords, so through the generic data API it reads, edits and deletes every member's view, exactly as it does sys_user_preference today. 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_default gets 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 as object.key view syntax (pinned).

  • Not view metadata: /meta/view does 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' in PLATFORM_OBJECTS_BY_PACKAGE['platform-objects'] (packages/spec/src/system/constants/platform-object-names.ts). Nothing else under packages/spec changes.

  • Rosters:

    • the CLI organization-ownership inventory row (fate: 'attribution');
    • the i18n extract config;
    • the en / zh-CN / ja-JP / es-ES object bundles, regenerated and then translated by hand;
    • 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's ViewScopeSchema.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)

  • M1, no stored default. The is_default column and the one-default hook are gone. The default is the Console's existing pin LIST_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, and ObjectView resolves it with resolveViewId(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 in views. A nanoid has no ., so the bare-key qualification step (<object>.<key>) never turns it into a metadata view's id. No second store.
  • M2, platform administrator. admin_full_access keeps 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.
  • S1. The object's comment now says a wildcard with viewAllRecords / modifyAllRecords still reaches a private object (ADR-0066 D2).
  • S2. External members are out of scope for this version: external_default gets no grant. Open question for the Console half.
  • Nits. An update that changes user_id is refused 403 PERMISSION_DENIED and 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 same created_by floor applies to sys_user_preference: by reading the source, its user_id is 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. A config.data.object that differs from object_name is refused 400 VALIDATION_FAILED on config.data.object, on create and on update (pinned, engine and REST; ablation 8).
  • Round 1's forced edit to durability-swallow-repair.test.ts is reverted: with no personal-view afterInsert left, the SCIM-stamp fake passes unmodified (22/22).
  • Merging main brought object.titleFormat has no effect when the object authors no nameField: the registry stamps a derived nameField that outranks the template, so the record title ignores it #29007, which retires titleFormat on code-shipped objects; sys_personal_view now names its title by nameField: 'label' alone (the sweep in managed-api-method-affordance-sweep.test.ts caught it).
  • The route name was accepted; the spec roster line is recorded as allowed; S3 (a shared own-row door) is the seat's follow-up card.

Open questions

  1. The platform administrator's reach. Through the generic data API, admin_full_access reads, edits and deletes every member's personal view, exactly as it does sys_user_preference (pinned as a recorded boundary). Options: A keep it, and decide it with sys_user_preference as 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.
  2. External members. external_default grants nothing on sys_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 + SQLite SqlDriver + SecurityPlugin.
    • Owner: saves, lists and renames their own views. The id has no .. 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).
    • Another member: findOne returns null, find returns [], count is 0, and an update answers 404 RECORD_NOT_FOUND. A create naming another owner is refused 403 PERMISSION_DENIED.
    • Admins: organization_admin and organization_admin_no_bypass both see none of a member's views (404). An organization admin keeps their own views (control). The recorded boundary: admin_full_access reads, edits and deletes a member's view through the engine.
    • Body: an invalid config is refused VALIDATION_FAILED, with paths under config, on create and on update, and the stored body is unchanged. Also covered: non-JSON text, and a rename without config.
    • The listed object: a create whose config.data.object is another object, an update moving only object_name, and an update sending only a body for another object are each refused on config.data.object, with the row unchanged. Control: changing both together is accepted.
    • The door's core (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.
    • It mounts exactly DELETE /api/v1/personal-views/:id.
    • An unreadable permission store answers its own 503. Anonymous gets 401. A held sign-in gets its policy's 403. A query parameter gets 400. No engine gets 503. An engine fault gets 500. In none of these is the engine asked.
    • On success, the removal runs as { 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.
    • Owner: saves, lists, reads and renames views, and cannot hand one to another member (403).
    • Another member: 404 / 404 / RECORD_NOT_FOUND, and the generic delete answers 403 PERMISSION_DENIED. A planted row naming another owner gets 403.
    • Organization admin (granted through sys_user_permission_set): 404.
    • Invalid body: 400 VALIDATION_FAILED; a body listing another object: 400 on config.data.object.
    • The predicate guard: a member whose field-level security hides showcase_project.budget can save a view filtering on budget, because a save does not re-judge field security. The list query GET /data/showcase_project?$filter={"budget":{"$gt":0}} is then refused 403, naming budget.
    • /meta/view does not serve it.
    • The door, [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 generic DELETE /data/sys_personal_view/<own id> is still 403.
  • Forced test edits (three; each is a list or a capture that the change itself moves):
    • packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts: the census of objects declaring defaultRowScope gains DECLARED_AT_BIRTH = { sys_personal_view }. It is kept separate from the ruling's 23 DECLARERS, which are unchanged. A new it.each asserts the literal and that no shipped set carries a copy.
    • packages/platform-objects/src/apps/translations/bundle-ownership.test.ts: OWNED_OBJECTS now names sys_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 reads PERSONAL_VIEW_ROUTE_LEDGER, because the door mounts on the IHttpServer port. 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 in scripts/cross-package-test-inputs.mjs and turbo.json, the same as the storage, i18n and settings ledgers.
Ablations (each red, then restored, git diff HEAD empty)
Mutation Red
Remove the organization_admin entry for sys_personal_view The organization_admin test (404 → row read). organization_admin_no_bypass stays green, as expected: that variant strips the bypass bits itself.
Remove defaultRowScope (platform-objects dist/ rebuilt, preflight confirmed absent) 4 tests: member read, plant, both organization-admin cases
Remove the ListView check (both hooks) 3 validation tests
Round 2: let a config.data.object that differs from object_name through 3 listed-object tests; the both-together control stays green
Round 2: switch off the fixed-owner rule The platform-administrator re-parent test. Over REST, the head before the rule (ee23b8e) is the red: the battery's [owner] cannot hand their view answered 200
Force the predicate guard condition false in plugin-security (dist/ rebuilt) The dogfood [predicate] test (200 instead of 403)
The door: drop the owner check (row.user_id !== callerId) The platform-admin case (the row was removed)
The door: read the row as the system instead of the caller 3 tests: another member and both organization-admin sets (403 instead of 404)
The door: AuthPlugin does not mount it (plugin-auth dist/ rebuilt, preflight confirmed the marker) 5 dogfood [door] tests. The generic-delete control stays green, as expected.

The /meta pin is structural, because no code path serves the object as metadata, so it has no mutation to ablate.

Verification

Round 2, on head deedb538 (main 81cd9eb0 merged; no conflicts):

  • Suites under os-verify-lock: spec platform-object-names.test.ts 12/12; platform-objects 110 files, 1512 tests; plugin-auth 215 files, 3693 passed / 10 skipped; plugin-security 241 files, 5203 passed / 45 skipped; cli organization-ownership-inventory.test.ts 26/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.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands listed 133 commands. All 133 exit 0 on deedb538 (check:type-check-debt run under os-verify-lock after the battery), reconciled with --ran: 133 derived, 133 run, 0 NOT-MEASURED.
  • Dogfood battery: pnpm --filter @objectstack/dogfood test under os-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-debt after it: exit 0.
  • The battery on the earlier round-2 head ee23b8ea had exactly one red, [owner] cannot hand their view (200, not 403). That was the re-parent gap the fixed-owner rule closes. CI on 9f7adf09 and d4773b47 found the census row and the retired titleFormat; both are fixed on this head.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

claude added 13 commits November 2, 2026 15:17
…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>
…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>
…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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:system tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/plugin-security, @objectstack/spec, touching 58 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/platform-objects/src/identity/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

98 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 81cd9eb0e9f2fb32a9ae01ef60dc4da45f892ef2.

⛔ 16 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/platform-objects/src/identity/index.ts) — pages documenting those are invisible to this run
  • 2 cross-cutting symbol(s) contributed no route anchor: isSystem (5 routes), packageId (4 routes)
  • 2 anchor(s) matched too much of the corpus to be a work list: PERMISSION_DENIED (literal, 51 pages), sys_user (literal, 48 pages)
  • 16 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 182 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 81cd9eb0e9f2fb32a9ae01ef60dc4da45f892ef2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 061d34e2bd151348b0277ed934f6b5968fa3ad3c — the merge of head deedb538e508cb01955a0a03192fa39a457a3ddb into base 81cd9eb0e9f2fb32a9ae01ef60dc4da45f892ef2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 81cd9eb0e9f2fb32a9ae01ef60dc4da45f892ef2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract-review-claim: session_01ADHkmQjPiXgNoSaEpqeGvY · head 805f048 · 2026-11-02T19:31Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 805f0481cdb41bd386ae7bb4b4710b9ff47f7cc2
Local-runs: none

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 origin/main. Nothing was built, run or re-run.

① Derived judgments

Accept-set and public-surface changes the diff implies

Change Judgment
New platform object sys_personal_view, served at /api/v1/data/sys_personal_view (read, create, edit) Right. It is on the sys_user_preference template that ruling C names. It is private, system-data, and declares the owner-only defaultRowScope with operation: 'all'. The organization column puts it behind the Layer 0 wall. The id is a nanoid, so it never takes the object.key form (pinned).
member_default grants r/c/e on the object and no delete Right. ADR-0090 D5 keeps delete off the anchor. The generic DELETE stays 403, pinned at both the engine and REST levels.
organization_admin names the object without viewAllRecords / modifyAllRecords Right. This is the sys_audit_log per-object pattern. resolveObjectPermission (permission-evaluator.ts) prefers the explicit entry, so the bypass is closed for both variants. Ablation 1 is red as described.
Save-time ListViewSchema check: 400 VALIDATION_FAILED with paths under config Right. It uses an existing code, refuses on create and on update-with-config, and leaves stored bodies unchanged (pinned).
is_default column, plus a hook that keeps one default per owner and object Wrong: M1. The card's In-list says "set as my default (reuse the existing pin)". The existing pin is the Console's per-object LIST_PINNED_VIEW_PREFERENCE (#27594, ObjectView.tsx), which already stores a view id. This PR adds a second, server-side default store beside it. Neither the PR body nor the dev report flags this as a deviation.
New route DELETE /api/v1/personal-views/:id Right in mechanism; see "Security of the door" below. The ledger row (gap), the route-envelope declaration and the single-authz-resolver delegator row are all present. The closed query set is empty and refused with 400 (AGENTS.md route rule 5). When no DELETE verb exists, the door is skipped with a warning that names it (rule 3).
PLATFORM_OBJECTS_BY_PACKAGE['platform-objects'] gains sys_personal_view Right. The registry's maintenance contract requires it, and #27123 and #27185 set the precedent. One side effect: stack.zod.ts now accepts a top-level action whose objectName is sys_personal_view (isPlatformProvidedObjectName). This is a widening, and #27123 classed it as "not an authorable surface".
CLI organization-ownership inventory row, i18n extract and 4 bundles, tenancy census (92 / 47), tenant-audit census (256) with its hand-written figures Right. Each one is derived, and the gates that own them are listed under check-runs below.

Security of the door

  • Refusal order is correct. The pre-read refusals (401, the held sign-in's 403, the query 400, 503) do not depend on the row. The row is then read as the caller, so an unknown id and an unreadable row get the same 404. A member or an organization administrator cannot use the door to learn that a row exists.
  • The 403 leaks nothing. It reaches only a caller who can already read the row (a platform administrator).
  • Attribution is right. attributedUserId is the caller and tenantId is the caller's organization, both pinned in personal-view-routes.test.ts.
  • Other read paths are closed.
    • Generic data API: row scope applies (pinned).
    • Export: the engine reads as the caller, and nothing grants allowExport on the object.
    • Search: searchable: false.
    • /meta/view: pinned 404.
    • expand: no lookup points at the object.
    • Realtime: no external transport is mounted on main.
    • Audit ledger: rows about the record are parent-record-gated (audit-log-read-visibility.ts), so an organization administrator without the ledger audit capability sees none of them, and delete rows are excluded.
  • The platform administrator is not limited to reading. This is M2. ADMIN_FULL_ACCESS_CAPABILITIES (spec/src/identity/eval-user.zod.ts) carries allowDelete, allowEdit and modifyAllRecords on '*'. The owner_only_* policies bind org_member only. So by the cited code, admin_full_access also PATCHes and DELETEs a member's view through /api/v1/data/sys_personal_view/:id. (This was read from source, not driven.) No test pins that path.
    • That makes three statements false: the PR body's 「但他删不了(答 403)」 ("but he can't delete it, answers 403"), the risk line, and the open question's premise "The delete door already refuses its delete with 403".
    • The door's 403 is still correct for the door itself. It just does not stop the platform administrator.

The four forced test edits are legitimate. None loosens a guard.

  • default-row-scope-declarations.test.ts: the ruling's 23 DECLARERS and their length pin are unchanged. The new DECLARED_AT_BIRTH set adds a stronger assertion: the literal is exact and no shipped set carries a copy.
  • bundle-ownership.test.ts: this is the guard's own instruction for a new extract-config object.
  • route-ledger-live-mount-parity.dogfood.test.ts: it adds a ledger to the union and exempts nothing. The input is declared in cross-package-test-inputs.mjs and turbo.json.
  • durability-swallow-repair.test.ts: the SCIM stamp really does register under packageId: 'com.objectstack.plugin-auth' (auth-plugin.ts on main), and the assertions are unchanged.

Check-runs on this head

There are 65 runs: 58 success, 7 skipped, and none failed or pending.

  • The success runs include every Test Core shard (1/8–8/8), the four Type Check families, every Lint family including PM dispatch-gates, Build Core, Build Docs, Build UI Deps, Live E2E UI, Dogfood Regression Gate, Check Changeset, Spec property liveness and Governed Surface Queue Guard.
  • The 7 skipped runs are opt-in or matrix-gated: UI Published Artifacts, Temporal Conformance, the Schedule gate, Packed-tarball smoke, Dogfood Verify CLI, the Dogfood shard matrix and the 18.x upgrade harness.
  • The gates are green. Neither must-fix below is something a gate measures.

Must-fix

  • M1: the is_default default store is an undisclosed deviation from the card. The card says "reuse the existing pin". There are two ways to fix it:

    As the PR stands, the ui lane gets two "my default" stores and no rule for which one wins.

  • M2: the PR states the platform-administrator boundary wrongly.

    • Pin what is really there: as admin_full_access, the generic GET, PATCH and DELETE on a member's view, beside the existing door-403 pin.
    • Correct the 维护者速读 (maintainer quick-read) risk line, the What-changes bullet, the changeset bullet (which says "still reads" and omits edit and delete) and the open question's premise.

Should-fix

  • S1: a code comment overstates what the private posture does. sys-personal-view.object.ts says "An application's wildcard set never widens who reads another reader's views". That holds only for a non-superuser wildcard. ADR-0066 D2 lets a '*' entry that carries viewAllRecords or modifyAllRecords reach a private object. Say "a plain (non-superuser) wildcard".
  • S2: external members get no grant. external_default (EXTERNAL_DEFAULT_PERMISSION_SET) replaces member_default for org_external and carries sys_user_preference but not sys_personal_view. An external reader therefore cannot save a view, while the card's ui step says "save-as-my-view available to any reader". Either add the same r/c/e line there, or record externals as out of scope so the ui lane hides the button for them.
  • S3: the door duplicates the unreact door. personal-view-routes.ts is a near line-for-line second copy of comment-reaction-routes.ts (caller resolver, handler and the removal core). File a follow-up to extract one shared own-row removal door before a third copy appears. This does not block the PR.

Nits

  • If M1 keeps the default rule, keepOneDefault does not hold under concurrent writes or a predicate update, which carries no id. That contradicts the hook's "two tabs cannot leave two defaults".
  • config.data.object is not checked against object_name.
  • An owner re-parenting user_id on update is held by the row scope's post-image check, but nothing pins it.

② Semver level

③ Boundary flags

Dev's open question: should admin_full_access keep reading members' views?
Answer: A for this card, kept as a recorded boundary and decided together with sys_user_preference. Platform administrators are the deployment's operators, and the rescue path depends on the bare wildcard. But the premise has to be corrected first (M2): today the boundary is read, edit and delete through the data door, not read only. If the maintainer is asked, the question must say so.

Dev flags (deviations)

  1. Spec in the changeset: kept; see ②. The card forbids a spec schema key. This is a registry row, not a key.
  2. The four forced test edits: legitimate; see ①.
  3. Route name /api/v1/personal-views/:id: recommend accept.
    • It is plural and kebab-case per AGENTS.md naming ("REST endpoints → plural"; compare /api/v1/share-links).
    • It takes the record id in the path, with an empty, refused query set, like the unreact door.
    • It cannot sit under /api/v1/data/… (route rule 1, one route, one owner).
    • This is the seat's call.
  4. Base Clause-② read from source: accepted. On base, no registrar names the route, and the object is not registered.
  5. The /meta pin is structural, and the no-bypass pin does not react to ablation 1: accepted as explained.
  6. Dist ablations rebuilt with OS_SKIP_DTS=1: noted. CI on this head is the gate.
  7. Earlier base-red batteries fixed by test(dogfood): re-point the runAs and anonymous-guest flow pins to flow start admission's record rule #29451: not re-judged. The final battery ran on this head.
  8. Fork 2 moved from B to A mid-card: matches seat ruling 6583289013. The PR says Fixes #29443 for the server step.

Escalated

  • The review tier is in doubt. record-recognisers.mjs --paths reads the schema face (CONTRACT_REVIEW_TIER) off packages/spec/src/system/constants/platform-object-names.ts. This record is the TIER_DEFAULT declaration review the brief ordered. The seat should confirm whether the next head also owes a CONTRACT_REVIEW_TIER record.
  • The claim names no spec exception. The claim (6580568459) says "⛔ No packages/spec/**", and the PR cites a seat ruling allowing the one-line spec touch. No such ruling is on the card. The card body forbids only a spec schema key, which this is not. The seat should record the allowance on the card.

Implemented-by: claude/project-thread-xtjloc
Reviewed-by: session_01ADHkmQjPiXgNoSaEpqeGvY

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>
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>
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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract-review-claim: session_01ADHkmQjPiXgNoSaEpqeGvY · head deedb53 · 2026-11-02T22:23Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: deedb538e508cb01955a0a03192fa39a457a3ddb
Local-runs: none
Scope: packages/spec/src/system/constants/platform-object-names.ts + packages/spec/src/system/constants/platform-object-names.test.ts, packages/spec/src/stack.zod.ts, packages/lint/src/validate-object-references.ts, packages/platform-objects/src/identity/sys-personal-view.object.ts, packages/plugins/plugin-auth/src/manifest.ts

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 TIER_DEFAULT (declaration face). Read: card #29443 (body and all 8 comments, including round-1 record 6586971553, the seat's round-2 ruling 6587007982 and dev report 6589962356), the PR body, the file list, the net diff against main, and the check-runs on this head. The cited code was read on origin/main at 5bd615ef. Nothing was built, run or re-run.

① Derived judgments

Round-1 findings and ruling points on this head

Item Status on deedb538
Ruling 1, spec allowance Met. The only packages/spec/** path in the file list is the roster line; one line, alphabetically placed (sys_organization to sys_platform_setting). @objectstack/spec: patch is in the changeset.
M1 / ruling 2, one default store Met. No is_default column and no one-default hook remain in the diff. The object's docblock, the hooks' docblock and the changeset all say the default is the Console's pin. The dev's claim about the pin is true on main: LIST_PINNED_VIEW_PREFERENCE (ui/packages/app-shell/src/utils/workspacePreference.ts) stores {"view": VIEW_ID, "v": 1} and fromRecord accepts any non-empty string; ObjectView.tsx resolves it with resolveViewId(pinnedViewChoice, views.map(v => v.id), objectDef.name), whose first step (ui/packages/core/src/utils/resolve-view-id.ts) is an exact-id match, and the two qualification steps only ever add or strip the OBJECT. prefix, so a nanoid with no . is matched as itself or not at all. It works once the ui half lists personal views in views; no second store.
M2 / ruling 3, platform administrator Met. admin_full_access is untouched. The generic GET, PATCH and DELETE of a member's view are pinned at the engine (personal-view-hooks.test.ts, two RECORDED BOUNDARY tests) and over REST ([platform-admin] RECORDED BOUNDARY). The PR body, the changeset bullet and open question 1 now say read, edit and delete; the false 403 sentence is gone. Only the door refuses the platform admin, as the body says.
S1 / ruling 4, object comment Met. sys-personal-view.object.ts now says a plain (non-superuser) wildcard does not reach a private object and one carrying viewAllRecords / modifyAllRecords still does (ADR-0066 D2), naming both sets.
S2 / ruling 5, external members Met. external_default is untouched (no sys_personal_view line in the diff). The PR body and the changeset say externals get no grant in this version; it is open question 2 for the ui half.
Ruling 6, route name Accepted by the seat; DELETE /api/v1/personal-views/:id unchanged.
Ruling 7 nits Met. The user_id re-parent refusal is pinned (engine, member and platform admin; REST, owner) with an echo control. The config.data.object vs object_name check is a refusal, 400 VALIDATION_FAILED on config.data.object (code object_mismatch), on create and on update (three pins plus a control at engine level, one over REST).
S3 Follow-up, the seat's card. Not in this PR, as ruled.
Round-1 forced edit to durability-swallow-repair.test.ts Reverted; three forced test edits remain and were judged legitimate in round 1.

Accept-set and public-surface changes the diff implies

Change Judgment
PLATFORM_OBJECTS_BY_PACKAGE['platform-objects'] gains sys_personal_view Right. The roster's maintenance contract (module header on main) requires the line, and platform-object-names.test.ts holds each group equal to the objects its package's *.object.ts files declare, so the line and the new object file are one change. Derived accept-set widenings on main: isPlatformProvidedObjectName now admits the name at rung ③ of validate-object-references.ts, in validate-nav-access.ts and validate-translation-references.ts, and stack.zod.ts accepts a top-level action whose objectName is sys_personal_view. Each is a reference to a real registered object, the #27123 / #27185 class; none is an author-writable key. dangling-reference-audit.ts reads only the plugin-security group, so it is unchanged.
New platform object served at /api/v1/data/sys_personal_view (read, create, edit) Right. On the sys_user_preference template: private, system-data, owner-only defaultRowScope with operation: 'all', organization column, nameField: 'label'.
The new save rule: a non-system update that changes user_id is refused 403 PERMISSION_DENIED, platform administrators included Right, and the diagnosis is correct on main. collectRLSPolicies (security-plugin-object-inputs.ts, header in security-plugin.ts and object-default-row-scope.ts) applies an object's defaultRowScope only when the held collection for that object AND operation is empty, and a held '*' policy counts. member_default carries owner_only_writes (object: '*', operation: 'update', created_by == current_user.id, positions: ['org_member']). So for a member's UPDATE on sys_personal_view the collection is that floor alone, and the object's user_id == current_user.id scope never enters the update gate: pre-image and post-image (rls-compiler.ts, check defaults to using) both pass on a row the member created, whatever user_id becomes. Reads and creates still collect the default (no held '*' policy for select or insert), which is why the row stays unreadable to others and a planted create is refused. A save rule is the right instrument: the floor is member_default's and platform-wide.
Can any path still re-parent a view? No non-system path found. The hook reads ctx.previous, and on main every beforeUpdate dispatch binds it: the by-id path reads the prior row before the dispatch and refuses an absent row with 404 (engine.ts, the #7867 gate); the predicate path dispatches per matched row with that row as previous (engine-hook-dispatch.ts#dispatchPerRowBeforeHooks); the whole-operation dispatch (dispatchUnscopedMultiWriteHooks) reaches only hooks flagged dispatchUnscopedMultiWrite, which this one is not, and the per-row dispatch still follows it. The REST import and the generic batch (packages/rest/src/data-action-routes.ts, runImport) write under the caller's own context (isSystem is set only on the sys_import_job rows), so an upsert's update is judged as the caller. A create naming another owner is refused by the row scope (pinned).
The isSystem skip Sound as a platform-writer skip, incomplete as stated. buildSession carries isSystem only from the execution context (engine-execution-plumbing.ts). No shipped system writer updates sys_personal_view; the door deletes. But app-authored automation elevated to system does carry isSystem: a runAs: 'system' flow, a hook's runAs: 'system' / sudo(), an action body (flow-system-identity-write-refusal.ts describes the family, and refuses it for four identity objects only). Such a flow can re-parent a view. It is admin-authored metadata, and the platform admin's reach is the ruled posture, so this is not a hole in the rule; it is a sentence missing from census row 26c and the hooks' docblock. S1 below.
Census row 26c Right in substance: owner, column, what is gained and lost, and the reason, with the generated register row (the gate check:system-context-census is in Lint & Repo Gates, green). The "no shipped system writer does" clause is true; S1 adds the authored family.
Does refusing the platform administrator's re-parent contradict ruling point 3? No. Ruling 3 keeps admin_full_access as it is and asks that the boundary be stated truthfully. The set is untouched, and the admin still reads, edits and deletes through the data door (pinned). The save rule is an object invariant that binds one column for every non-system caller, stated in the PR body, the changeset and row 26c. A narrower rule on a brand-new object, stated, is within the ruling.
organization_admin entry without bypass bits; member_default r/c/e no delete; the door; ledger, envelope and resolver rows; CLI inventory, i18n, both censuses Unchanged from round 1 and judged right there. The tenant-audit census figures (255 to 256, 78 to 79, 145 to 146) follow the one new elevated write.
Merge of main with #29007 (titleFormat retired) No regression. The new object declares nameField: 'label' and no titleFormat (the comment on sys_user_preference still mentions the word in prose only). managed-api-method-affordance-sweep.test.ts on main sweeps every code-shipped object for the retired key; Test Core is green on this head.

Check-runs on this head: 69 runs, all completed; 60 success, 9 skipped, none failed, none pending. The required set is green: Lint & Repo Gates, TypeScript Type Check, Test Core (all 8 shards), Build Core, Governed Surface Queue Guard, Lint UI, Type Check UI, Test UI (all 16 shards), Build & E2E UI, Live E2E UI. Check Changeset ran twice (the second run on the PR-body update) and concluded success both times. The skipped runs are opt-in or matrix-gated as in round 1.

② Semver level

  • patch on all five packages holds. The level follows the kind (Metadata-protocol: none takes patch, AGENTS.md Post-Task Checklist); platform-objects: sys_exchange_rate { currency, rate, effective_date, source } — tenant-scoped, admin-only, audited; Setup list view (multi-currency M1-1, epic #8345; released with M2) #27185 is the direct precedent for a new platform object. @objectstack/spec stays in the changeset by ruling 1.
  • Clause-②: yes (widening) holds. A new served object, a new served route, and no existing answer narrows: the generic DELETE is still 403 for a member (pinned at both levels), and the member_default, external_default and admin_full_access sets are unchanged. The one refusal the PR adds on a path that answered 200 on an intermediate head (PATCH user_id) is on the new object only, so nothing accepted on main is refused.
  • Metadata-protocol: none holds; /meta/view does not serve the object (pinned).
  • The changeset prose now matches the tree: the platform administrator reads, edits and deletes; externals get no grant; the owner is fixed on save. Both declaration lines appear in the PR body and in the changeset, and Check Changeset is green.

③ Boundary flags

Dev open question 1, platform administrator's reach: answered by ruling 3, option A; kept as a pinned recorded boundary and decided with sys_user_preference as one question.

Dev open question 2, external members: answered by ruling 5; out of scope here, the ui half's question.

Dev deviations (round-2 report)

  1. The fixed-owner rule is a save rule, not a row-scope change: right; the floor is member_default's and platform-wide, out of this card's reach.
  2. Three forced test edits, the fourth reverted: legitimate (round 1), and the revert is correct since no afterInsert remains.
  3. Docs row 26c plus its generated register: accepted, with S1.
  4. The intermediate battery stopped after CI found titleFormat; the final battery ran on this head: not re-judged; CI on this head is the gate.

Dev out-of-scope finding: sys_user_preference has the same re-parent gap. Confirmed by reading main; it is real. sys-user-preference.object.ts: user_id is a lookup, not readonly, and no hook names the object (plugin-auth and plugin-security register none; identity-write-guard.ts guards sys_user only). member_default carries both owner_only_writes ('*', update, created_by == current_user.id, org_member) and sys_user_preference_self (operation: 'all', user_id == current_user.id). RLSCompiler.compileFilter OR-combines the policies of one object and operation, so on a by-id update the pre-image passes on either arm and the post-image passes on created_by. So an organization member can PATCH /api/v1/data/sys_user_preference/ID with another member's user_id, and the row then reads under the victim's _self policy: a planted preference, for any key the victim has not set (the (user_id, key) unique index per organization blocks keys they have). The consumer is ui/packages/data-objectstack/src/userState.ts. Not driven here, by this brief's rule; the seat files it as a card. The remedy is the same shape as this PR's rule, or a platform-wide answer for every object whose owner column is not created_by.

Escalated: nothing. Both round-1 escalations (the tier and the spec allowance) are closed by ruling points 8 and 1.

④ Design judgment

packages/spec/src/system/constants/platform-object-names.ts, judged against the registry's own contract on main and its first consumers. The contract says adding an object to a platform package means adding its name here, and the conformance test holds the platform-objects group byte-equal to the declared *.object.ts set, so the line is forced by the object file and could not be omitted. Its consumers are predicates (isPlatformProvidedObjectName) that turn a reference to the name from a typo finding into a resolved reference; the one stack.zod.ts consumer accepts a top-level action naming the object, which the object's row scope and private posture then govern at run time. The object it vouches for is well-formed on this head: nameField only (no retired titleFormat), owner-only default scope, private, organization-scoped, registered by one manifest under plugin-auth's package id, and no shipped set carries a copy of its scope (default-row-scope-declarations.test.ts, DECLARED_AT_BIRTH). I tried to break it two ways: a name collision (none; sys_personal_view appears nowhere on main) and an ordering drift (the test sorts both sides). No finding.

The rest of the diff at TIER_DEFAULT, judged on the tree:

  • The save rule's shape is right: it refuses before any write, names the row, keeps the ADR-0112 pair (PERMISSION_DENIED / 403), treats an echo as no change, and is registered by both registrars so it travels with the table. IdentityObjectsPlugin.init() refuses a composition beside AuthPlugin on main, so the hooks are never registered twice.
  • The door is unchanged from round 1 and was judged there (refusal order, read-as-caller, attribution, 404 parity).
  • Tests: the platform-admin re-parent pin isolates the rule from the row scope (the one caller no scope binds), the engine [owner] pin and the REST [owner] cannot hand pin cover the member, and the echo control keeps the rule from refusing a rename that resends the owner.

Must-fix: none.

Should-fix

  • S1. State the authored-elevation family in row 26c and the hooks' docblock. "No shipped system writer does" is true, and incomplete: a runAs: 'system' flow, a runAs: 'system' or sudo() hook and an action body carry isSystem (the family flow-system-identity-write-refusal.ts names) and can re-parent a view. Say that the rule does not bind app-authored automation elevated to system, which is admin-authored and the same posture sys_user_preference has, so the row says what is really lost. One sentence each; no code change.
  • S2. Stale comment in packages/plugins/plugin-auth/src/manifest.ts: "Its save validation and default flag live in personal-view-hooks.ts". The default flag is gone (M1). Say "save validation and fixed-owner rule".

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: claude/project-thread-xtjloc
Reviewed-by: session_01ADHkmQjPiXgNoSaEpqeGvY

VERDICT: PASS

Stamped 2026-11-02T22:33Z.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

席位核对 · ACCEPT · domain:server seat 1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY · 2026-11-02T22:35Z

Head deedb538e5, round-2 report 6589962356 (on #29443), claim 6580568459. Clause-②: yes (widening) plus one spec roster path, so the record is owed at CONTRACT_REVIEW_TIER (seat ruling 6587007982 point 8). Round 1 was record 6586971553 at TIER_DEFAULT, FAIL (M1, M2). Round 2 is record 6590198210 on this head at CONTRACT_REVIEW_TIER, PASS, with no must-fix and every round-1 finding and ruling point met.

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.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 22:36
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 22:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 4008e62 Nov 2, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-xtjloc branch November 2, 2026 23:01
os-steve pushed a commit that referenced this pull request Nov 3, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

1 participant