Skip to content

console: the permission-set editor keeps the clone notice "The copy is held by nobody yet" after users are assigned and the copy is saved #13272

Closed

Description

@objectstack-fleet

Filing gate ③ a task the maintainer set: objectstack-ai/hotcrm#2207, the maintainer's browser round on ObjectStack 18.0.0 (「hotcrm 升级到 objectstack 18.0 然后重新安排一轮浏览器测试」). Each defect gets its own card, by the maintainer's ruling 「按照按照具体的issue单独提交卡片,不受数量限制。」 (recorded on objectstack-ai/hotcrm#2188, 6283467378). This is finding F-06 of track 2 (the Setup pass as the system administrator), by the cloud tester session_01BkRPwuAufrFwoEEfuigDm9. It was measured on HotCRM main 16c28664 with @objectstack/* 18.0.0 (framework 06cab4849, console objectui 616df6598b42), in Chromium at 1440×900 on a locally booted, seeded install. The evidence is on the branch claude/qa-v18-setup-pass at 4d7cc467d (REPORT.md), which is never merged. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.

Reader (who acts): objectui triage routes it.

The finding, as measured

  • Persona / URL: dev admin, /apps/setup/metadata/permission/qa_fls_rep, made by Clone to customize from sales_rep.
  • Steps: Assigned Users → Add user (QA Position) → Confirm (201); change a field override; Save (200).
  • Expected: the banner reflects the state. Its own last sentence is a call to act: "The copy is held by nobody yet: add it to a position, or assign it to users."
  • Actual: the banner stays, unchanged, after the assignment and after Save, while the panel below it reads "Assigned Users 1 — QA Position, direct", and that user reads accounts through the set.
  • Evidence: 14-access-permission-sets-cloned.png, 14-access-permission-sets-fls-saved.png.
  • Reach: admins who clone a permission set.

Seat review (source at the console pin 616df6598b42)

  • packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx:
    • stores the clone result on onCloned (:1226, setCloned(copy));
    • keeps it for as long as the copy is the open set, across every reload (:685);
    • renders the server's notice verbatim from it (:1414).
  • Nothing clears it on an assignment or a save.
  • The sentence is a point-in-time server notice (objectstack packages/runtime/src/domains/permission-set-clone.ts:121), so the editor is what makes it stale.
  • Not verified: the tester's "it re-appears as a toast on Save". The editor makes no toast call, and the saved-state screenshot shows no toast.
  • The only commit to the file after the pin (c8833c5b8d, unsaved-edit prompts) does not touch this.

Dedupe

Method: every objectstack-ai/objectstack issue title (every state, #1–#24932) and every objectstack-ai/objectui issue title (#7–#13268), listed through the repo-scoped issues?state=all endpoint (refreshed with since=2026-10-19) and matched case-insensitively. The search API is refused for this session.

  • Related, closed: objectui#5987 (introduced Clone to customize and this banner).
  • Different: objectstack#24327 (the notice's wording); objectui#12164 (the clone door).
  • New.

Generated by Claude Code

Activity

  1. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 18 (priority:p3)
    Session: session_01XVp19E1MYUTFM3dnkwz8gB
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-13272-clone-notice-stale
    Worktree: objectui-issue-13272
    Domain: domain:ui
    Seat: domain:ui#2
    File surface:

    • packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx (the cloned state, ~685, ~1226, ~1414);
    • tests beside it;
    • one .changeset/13272-clone-notice-stale.md (@object-ui/app-shell patch).

    ⛔ No server change; no new export. Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (default tier; no path-derived mandate)
    Clause-②: no (internal editor)
    Responsibility: the editor keeps the clone result's point-in-time notice for as long as the copy is open | the holders panel already knows the set's assignments | every admin who clones a permission set and then assigns or saves it
    Thread-read: none
    Serial constraints cleared:

    • objectui#13253 (c8833c5, PermissionMatrixEditor registers with the unsaved-edits seam) landed.
    • objectui#13119 (claimed with this card by this seat) edits AssignedUsersSection.tsx and PositionHoldersSection.tsx comments, not this file.
    • No open objectui PR touches PermissionMatrixEditor.tsx (read at 18:28Z).
      Claimed at: 2026-10-20T18:03Z

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 13272,
      "status": "needs_decision",
      "branch": "claude/issue-13272-clone-notice-stale",
      "pr": null,
      "session": "session_01XVp19E1MYUTFM3dnkwz8gB",
      "premise_still_valid": true,
      "summary": "Premise holds on origin/main 8cd4db4974: `cloned` is set in onCloned, kept across reloads of the same name in the load effect, and rendered verbatim; nothing clears it on a save or an assignment. Ruling 2 (save) and pins (b) and (c) are done and pushed as b64bdc863d (Resume-from: claude/issue-13272-clone-notice-stale@b64bdc863d): a successful save OF THE COPY clears the notice (functional setter keyed on payload.name === cloned.name, after the outcome is not 'reloaded'); a refused save keeps it; on an embedded host, where the notice sits on the base, saving the base keeps it. Ruling 1 (first holder) is NOT implemented: the editor does not know the set's holders, only AssignedUsersSection does (its rows are direct grants plus holders via positions that distribute the set), and that component has no callback. Wiring it needs an edit to AssignedUsersSection.tsx, which is outside the claimed file surface (claim: stop on breach), so I stopped there and opened no PR (the order requires `Fixes #13272`, which a half fix would wrongly close). The notice is one server string with three sentences; I clear it whole rather than cut out the last sentence, because cutting it would key the editor on the server's prose (ruling 3). The first two sentences (no link to the source; adminScope and isDefault not copied) stay true after a save but they describe the clone moment, so nothing in the banner is kept after a save.",
      "tests": "Under os-verify-lock (slot issue-13272), in the worktree at b64bdc863d: `pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx` gave VERDICT command-exit 0, `Tests 24 passed (24)`: 20 existing tests plus 4 new ones in describe 'the clone notice is point-in-time: a saved copy is past it (objectui#13272)'. The 4 new tests: (c) CONTROL, the notice textContent equals the server string right after a clone; (b) clone then Save the copy, notice gone; a refused save keeps it (control); embedded, saving the base keeps it (control). Pin (a) was not written (see open_questions). Ablation with objectstack scripts/ablation-replace.mjs --delete of the one setCloned line in doSave: the anchor went x1 to x0, blob 5080253daf3f to 413e728c280a, and the result was `Tests 1 failed | 23 passed (24)`, the failure being only 'clone, then Save the copy: the notice is gone'. Restore was proven by the tool: blob after restore 5080253daf3f == blob at HEAD 5080253daf3f, and `git diff HEAD` empty. Re-checked by hand: git hash-object == git rev-parse HEAD:path == 5080253daf3f788b912aca6e4691df64c662a5f5. The ablation's notice-gone assertion runs on src, so no dist was involved (relative import, no build). Type-check: closure build `turbo run build --filter='@object-ui/app-shell^...' --concurrency=2` gave exit 0 (28/28, all cached); `pnpm --filter @object-ui/app-shell run type-check` (tsc --noEmit plus tsc -p tsconfig.test.json) gave VERDICT command-exit 0; `tsc -p tsconfig.test.json --listFilesOnly` lists the edited test file (1 hit). Lint narrowed to the 2 changed source files: `eslint --no-inline-config --format json` read 2 files, 0 errors, 22 warnings, all on lines this diff does not touch. Population: eslint.config.js applies to `.`. Invariance: eslint.config.js enables no type-aware linting (no typeChecked config, no parserOptions.project), so this diff cannot move a verdict on any untouched file. Gates all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes; scripts/check-hand-rolled-comment-mask.mjs; scripts/check-changeset-fixed.mjs; scripts/check-changeset-presence.mjs. Declared to CI: the full packages/app-shell directory (no whole-directory run here, per the order). check:spec-floors was not run (not in the battery).",
      "mcp_calls": "0",
      "api_writes": "1: POST /repos/objectstack-ai/objectui/issues/13272/comments (this os-dev-report, via post-stamped and the fleet-write relay). Also 2 git pushes (the empty-branch probe and b64bdc863d), which are not REST writes. No PR, no label write.",
      "open_questions": [
        {
          "question": "Ruling 1 (clear the notice when the copy gains its first holder) needs the editor to learn the holder count from AssignedUsersSection, which is outside the claimed file surface. Which route?",
          "options": [
            "A: widen the surface to packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx. Add one optional prop, e.g. onHoldersRead(count), called after each successful load. The editor clears `cloned` when the count for the copy is above 0. The rows already union direct grants and holders via positions, so this one signal covers both sections. The component is not exported from the package entry and has one consumer, so it adds no new export. Cost: about 10 lines plus pin (a) and its ablation; the edit has to be serialized with objectui#13119, which edits comments in the same file. Axes: business need, measured: the tester's exact path (Assigned Users, Add user, banner stays). Long term: the section that owns the holders reports them, contract-first, with no second read. AI-error: one declared signal, nothing inferred. Startup scope: no new capability, an internal prop only.",
            "B: stay inside the surface. The editor subscribes to the data-invalidation bus (useDataInvalidation('sys_user_permission_set')) and clears the notice on the first grant write while the copy is open. Cost: about 8 lines, no surface change. Axes: it infers 'gained a holder' from 'some grant row changed'. That is exact on this page today only because the section is the only grant writer mounted, and it fires only if the host mounts useMutationInvalidationBridge. An embedded host without the bridge would fail silently. Long term: a heuristic coupling, the tolerant shape AGENTS #0.1 warns against.",
            "C: land the save half alone as `Part of #13272` and file the holder half as a follow-up card."
          ],
          "recommendation": "A, on all four axes. It reads the fact from its owner, adds no capability and no export, and costs one serialized edit with objectui#13119. B trades correctness on hosts without the bridge for staying inside the surface. C splits a small fix across two rounds."
        }
      ],
      "out_of_scope_findings": [],
      "deviations": "No PR opened, although the order requires one: the work stopped on a file-surface breach (claim: stop on breach), and a `Fixes #13272` PR would close the card on a half fix. The branch carries the save half at b64bdc863d. The worktree is removed: everything is pushed, so a resume starts from the remote branch.",
      "gates": "type-check app-shell 0; vitest (edited file) 0; eslint changed files 0; check:phantom-deps 0; check:self-import 0; check:unreferenced-sources 0; check:doc-example-readers 0; check:handler-key-reads 0; check:side-effects-array 0; check:element-data-source-declaration 0; check:spec-symbols 0; check:action-forward-parity 0; check:designer-field-key-parity 0; check:icon-record-names 0; check:i18n-keys 0; check:i18n-drift 0; check:i18n-designer-parity 0; check:control-bytes 0; check-hand-rolled-comment-mask 0; check-changeset-fixed 0; check-changeset-presence 0; check:spec-floors NOT MEASURED (not run)",
      "files_changed": [
        "packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx",
        "packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx",
        ".changeset/13272-clone-notice-stale.md"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 13272,
      "status": "done",
      "branch": "claude/issue-13272-clone-notice-stale",
      "pr": "https://github.com/objectstack-ai/objectui/pull/13282",
      "session": "session_01XVp19E1MYUTFM3dnkwz8gB",
      "premise_still_valid": true,
      "summary": "Addendum to the needs_decision report (comment 6298182676), after the PM's ruling A. AssignedUsersSection gains one optional prop, onHoldersRead(count, permissionSetName). The section calls it after each successful holders read with the count it lists (direct grants plus holders via a position, one per person) and the set that read was for; a failed read reports nothing. The editor clears the clone notice when a read for the copy reports more than 0. It keys on the reported set name, so a late read of another set cannot clear it. Together with the save half from b64bdc863d, the notice now goes once the copy is held or saved, and goes whole: it is never cut by its prose. The component is not exported from the package entry and keeps its single consumer. objectui#13119 (PR #13280) landed first and was merged into the branch at 3d9a508739; the merge was clean. Edge, not changed: a copy that is bound only to the `everyone` anchor position counts 0 holders, because the section lists no rows for it and shows a note instead. Its notice therefore stays until the copy is saved, following the ruling's count of what the section shows.",
      "tests": "At 3d9a508739, under os-verify-lock (slot issue-13272), with --maxWorkers=1: the 42 test files that name PermissionMatrixEditor or AssignedUsersSection (from git grep) gave `Test Files 42 passed (42)`, `Tests 337 passed (337)`. The full packages/app-shell directory is declared to CI. New pins: (a) clone, then add a holder through the section stub (which reports each read the way the section does), and the notice is gone; CONTROLs: a copy read reporting 0 keeps it, and the base's holders do not retire it. (b) and (c) are as before. Section pins: it reports the union count 2 with the set name; it reports 0 for an unheld set (control); it reports again after an Add; it does not report on a failed read. Ablations used objectstack scripts/ablation-replace.mjs, and every restore was proven as blob == HEAD with `git diff HEAD` empty, re-checked by hand with git hash-object. (1) Deleting the doSave setCloned line gave 1 failed | 23 passed, the failure being (b) only; blob 5080253daf3f to 413e728c280a, restored to 5080253daf3f. (2) Setting the editor's prop to `onHoldersRead={undefined}` gave 1 failed | 38 passed, the failure being (a) only; blob 5afa76e8dd39 to b569fbf6683f, restored to 5afa76e8dd39. A first attempt replaced the anchor with ' />', which already occurred 49 times. The tool refused it before running anything ('a rise of 0, not the declared 1') and restored the file, so that attempt was a no-op and was re-run with a unique replacement. (3) Deleting the section's call gave 3 failed | 36 passed, the 3 section report pins; blob 3db1559a1b78 to 01eab631b35f, restored to 3db1559a1b78. The editor pin uses a stub of the section, so (a) is reddened by leg 2 and the section's own pins by leg 3. Type-check at 3d9a508739: the closure build turbo --filter='@object-ui/app-shell^...' exited 0 (28/28), and `pnpm --filter @object-ui/app-shell run type-check` gave VERDICT command-exit 0. Lint: `eslint --no-inline-config --format json` read the 4 changed TS files: 0 errors, 38 warnings. 2 of the warnings are new: react-hooks/refs on the new callback ref (the same pattern as metadataStoreRef beside it) and react-hooks/exhaustive-deps on the test stub's once-per-set effect. Population: eslint.config.js applies to `.`. Invariance: there is no type-aware linting, so untouched files are unaffected. Gates at 3d9a508739, all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, new-line-citations; check-hand-rolled-comment-mask; check-changeset-fixed; check-changeset-presence. CI: not awaited (in_progress at the report).",
      "mcp_calls": "0",
      "api_writes": "3 over the whole claim, all through the fleet-write relay: POST /repos/objectstack-ai/objectui/issues/13272/comments (report 6298182676); pr_create on objectstack-ai/objectui, which made PR #13282 (draft, assignee baozhoutao, body read back identical at 5298 bytes) plus its assignees leg; and POST /repos/objectstack-ai/objectui/issues/13272/comments (this addendum). There were 4 git pushes, which are not REST writes: the probe, b64bdc863d, 178250e003 and 3d9a508739. No label write: the order named none.",
      "open_questions": [],
      "out_of_scope_findings": [],
      "deviations": "The merge commit 3d9a508739 carries git's default message without the Claude-Session/Co-authored-by trailer pair. It is left as is, because rewriting it would need a force-push, which is banned. The write pace refused the relay twice (exit 10); both writes were retried after `write-pace --status` read 'may go'. The section ablation leaves editor pin (a) green by design, since the editor suite stubs the section; (a)'s red comes from ablation leg 2.",
      "gates": "type-check app-shell 0; vitest family (42 files) 0; eslint changed files 0; check:phantom-deps 0; check:self-import 0; check:unreferenced-sources 0; check:doc-example-readers 0; check:handler-key-reads 0; check:side-effects-array 0; check:element-data-source-declaration 0; check:spec-symbols 0; check:action-forward-parity 0; check:designer-field-key-parity 0; check:icon-record-names 0; check:i18n-keys 0; check:i18n-drift 0; check:i18n-designer-parity 0; check:control-bytes 0; check:new-line-citations 0; check-hand-rolled-comment-mask 0; check-changeset-fixed 0; check-changeset-presence 0; check:spec-floors NOT MEASURED (not in the battery)",
      "files_changed": [
        "packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx",
        "packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx",
        "packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx",
        "packages/app-shell/src/views/metadata-admin/AssignedUsersSection.test.tsx",
        ".changeset/13272-clone-notice-stale.md"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #13282 @ 3d9a5087390b92717bc2f196f082887e06bfd18a · domain:ui seat 2 (session_01XVp19E1MYUTFM3dnkwz8gB)

    • Clause-②: no. One optional prop on a component that is not exported from the package entry and has a single consumer; no export moves. No contract review is owed, and the seat read both reports.
    • Seat review: the clone notice is treated as point-in-time.
      • Save half: a successful save of the copy clears it. A refused save keeps it, and an embedded host saving the base keeps it.
      • Holder half (surface amended by the seat, ruling A on the dev's needs_decision): AssignedUsersSection reports onHoldersRead(count, permissionSetName) after each successful read, with the union count it lists (direct grants plus holders via a position, one per person). The editor clears the notice when a read for the copy reports more than 0, keyed on the set name, so a late read of another set cannot clear it. A failed read reports nothing.
      • The notice goes whole and is never cut by its prose (ruling 3).
      • objectui#13119 (PR docs(app-shell): the organization administrator creates and removes Setup holders #13280) landed first, and the branch merged main cleanly at 3d9a508739.
    • Edge accepted: a copy bound only to the everyone anchor position counts 0 holders, because the section lists no rows for it. Its notice stays until the copy is saved, per the ruling's count of what the section shows.
    • Pins: (a) clone, then add a holder: the notice is gone, with CONTROLs (a 0-count read keeps it; the base's holders do not retire it); (b) clone, then save: gone, with a refused-save CONTROL; (c) CONTROL: right after a clone it shows verbatim. The section reports the union count with the set name, 0 for an unheld set, again after an Add, and nothing on a failed read. Three ablations (the save clear, the editor's prop, the section's call) each turned exactly their pins red, and each restore was proven by blob. The family (42 files, 337 tests) is green.
    • Changeset: @object-ui/app-shell patch.
    • CI: all 36 check-runs completed on the head, 33 passed and 3 skipped.
    • Path fork: no governed path. Seat landing. The merge commit carries git's default message; rewriting it would need a force-push, which is not done.
    • Findings: none.

    Next: ready + auto-merge; landing is verified by content on main.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB

    • Merged: PR fix(app-shell): retire the permission-set clone notice once the copy is held or saved #13282 through the merge queue as main cf153eb2e5.
    • Verified by content: each of the 5 files the PR changes has the same blob at cf153eb2e5 as at the accepted head. The merge message carries no model identifier.
    • Landed: the permission-set editor's clone notice ("The copy is held by nobody yet…") retires once the copy is held or saved:
      • Held: AssignedUsersSection reports its holder count for the set it read, and the editor clears the notice for the copy at the first holder.
      • Saved: a successful save of the copy clears it.
      • A refused save, a 0-count read, or the base's holders keep it. The server's text shows verbatim while it shows.

    pm:dispatched is removed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions