Skip to content

Studio: package-declared permission sets render locked, with Clone-to-customize as the only edit path (UI half of objectstack#11513) #5987

Description

@os-zhuang

History: this card was blocked by objectstack-ai/objectstack#19195 until 2026-09-27, then unblocked on the maintainer's word with a narrowed scope (see the triage comment of that date).

⚠️ The original Blocked-by: target, objectstack-ai/objectstack#11513, is CLOSED completed — the server half landed. It is replaced rather than deleted because a SECOND gate was found on top of it: the provenance predicate this card's own “Sequencing” section says must not be guessed is still unread, and objectstack-ai/objectstack#19195 is that reading. Everything this seat already measured is on the comment thread so it is not re-paid.

Filed by the triage seat executing the maintainer's 2026-08-24 ruling on objectstack#11513 (verbatim: 「同意 第一步(创业阶段,Salesforce 式)」— lock the base, clone to customize; the silent-fork save path closes server-side).

Scope (UI half)

In Studio / Setup, a permission set carrying package provenance:

  1. renders locked (read-only), with a visible marker saying it is package-declared and why it cannot be edited in place (upgrades keep flowing to it);
  2. offers Clone to customize as the primary action — producing an ordinary org-owned copy the user then edits;
  3. if the user somehow reaches the save path anyway, the server's refusal (the objectstack#11513 half) is surfaced as the same guidance, not a generic error toast.

Org-owned permission sets (including clones) are unaffected.

Sequencing

Blocked on the server half: the refusal shape and the provenance predicate it exposes decide what the UI keys the lock on — do not guess the predicate from the current row shape. Per the release-gating discipline, this dispatches when the framework change is installable (published spec/framework version covering it), not merely merged.


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 24, 2026
  2. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Unlock scan: the upstream is closed, but this card does NOT return to the queue

    The Blocked-by: target objectstack#11513 is closed / completed, landed by merged PR objectstack#11702 (merged 2026-08-24T14:01:37Z). Under a merge-only reading this card would go back to pm:queue. It must not — and this comment records the measurement so the next unlock scan does not re-derive it from scratch.

    This card's own sequencing clause already ruled it, quoted verbatim from the body:

    Per the release-gating discipline, this dispatches when the framework change is installable (published spec/framework version covering it), not merely merged.

    Measurement

    1. objectstack#11702 touches no file under packages/spec. Its 9 files are 7 under packages/plugins/plugin-security, one changeset, and one pinned contract JSON. So the dependency here is not a spec type — it is the server refusal shape and the provenance predicate, and those ship in @objectstack/plugin-security.
    2. That PR's changeset bumps '@objectstack/plugin-security': minor.
    3. Newest published @objectstack/plugin-security release: 17.2.0, published 2026-08-23T07:02:04Z — one day before the lock merged.
    4. Across the 100 most recent objectstack releases the maximum published_at is 2026-08-23T07:02:23Z. Releases published on or after 2026-08-24: none. Positive control in the same query shape: that same query does return @objectstack/plugin-security rows (17.2.0 and 17.1.0), so the zero is a measured zero rather than an empty query.
    5. This repo resolves @objectstack/spec@17.2.0 in pnpm-lock.yaml — the same 2026-08-23 train.

    So the refusal shape and provenance predicate a dev would key the lock on exist only on objectstack main, not in anything a consumer can install. Dispatching now would end premise-false: the dev would have to guess the predicate from upstream source, which this card's body explicitly forbids ("do not guess the predicate from the current row shape").

    State change

    pm:blocked is replaced by pm:on-hold. The body's Blocked-by: line is superseded by the restart criterion below and is left in place only as history — the card leaves the blocked population, so the now-closed upstream will not re-trigger an unlock attempt.

    Restart-when: npm view @objectstack/plugin-security version returns a version greater than 17.2.0 (published train is 17.2.0 from 2026-08-23T07:02Z; the packaged-permission-set save-door lock merged 2026-08-24T14:01Z and is in no published release).

    No Restart-touch: file is named, and that omission is deliberate rather than missing: objectui installs no objectstack runtime plugin, so no file in this repo moves when that release ships. The criterion above is executable and belongs to the daily executable-criterion sub-round.

    On restart the dispatch shape is unchanged from the body's Scope section; the first dev action is to read the shipped refusal payload and provenance predicate from the installed package, not from objectstack source.


    Generated by Claude Code

  3. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    ⛔ Not dispatched this round — the unlock this card is sitting on was never PROVEN, and this seat cannot prove it

    domain:ui execution seat, session_012EpHzwH4wTy5sd7ibkD2yq. This card is the oldest non-decision priority:p2 in the domain:ui queue, so it was the first thing looked at for this batch. It is being passed over, and the reason belongs on the card rather than in a session.

    What this card asks for, in its own words

    Blocked on the server half: the refusal shape and the provenance predicate it exposes decide what the UI keys the lock on — do not guess the predicate from the current row shape. Per the release-gating discipline, this dispatches when the framework change is installable (published spec/framework version covering it), ⛔ not merely merged.

    What is measured, and by whom

    ⭐ Measured, and it is encouraging: objectstack#11513 is closed / completed (2026-08-24, os-sam). The predicate the UI would key on is declared in objectstack's tree at packages/spec/src/security/permission.zod.ts:650:

    packageId: z.string().optional().describe('[ADR-0086 D3] Owning package id for a package-shipped set (absent = env-authored)'),
    

    and that checkout's packages/spec/package.json reads 17.4.0, which is exactly the version this repo's pnpm-lock.yaml resolves @objectstack/spec: ^17.0.0 to.

    ⛔ And none of that is the probe this card requires. Every line above is a reading of objectstack's source tree. The rule names a different instrument — 「安装面探针:装上的包是否接受新键/新行为」 — an installed package answering about a new key or new behaviour. A source tree and a published tarball are not the same object, and a version number matching is a correlation, ⛔ not an install-surface answer.

    ⚠️ This seat cannot run that probe: node_modules/ in this checkout is empty — ls node_modules | wc -l → 0, against a lit control of a pnpm-lock.yaml that resolves the pin to 17.4.0. ⇒ there is no installed @objectstack/spec here to interrogate.

    ⛔ Why this is not just 「dispatch it and let the dev check」

    The card's own sequencing sentence puts the installability gate before dispatch, and it is right to: a dev that finds the predicate absent has already spent a worktree, a branch and a round on a card that should never have left the queue. ⭐ And the card names the sharper risk itself — ⛔ do not guess the predicate from the current row shape. A dev who cannot see the real predicate is in exactly the position where guessing looks reasonable.

    What settles it, precisely

    One reading, by any seat with an installed tree: does the installed @objectstack/spec (and whatever the console consumes for the refusal shape) expose packageId on the permission-set schema, and does the save path return the refusal in a form the UI can key guidance on — as opposed to a generic error? A positive answer dispatches this card immediately; a negative one moves it to pm:on-hold with a Restart-when: naming the consumer-side install criterion, ⛔ not back to pm:queue.

    ⛔ State left untouched. It stays pm:queue, unassigned. ⛔ This seat did not move it to pm:blocked — its blocker is closed, and inventing a hold on an unmeasured premise would be the same error in the other direction.

    domain:ui execution seat · session_012EpHzwH4wTy5sd7ibkD2yq · dispatch-pass note · reading taken 2026-09-17T03:11Z


    Generated by Claude Code

  4. os-tesla commented on Sep 19, 2026

    @os-tesla
    Collaborator

    pm:queue → pm:blocked — the named blocker CLOSED, and a second gate was found underneath it

    domain:ui#2 execution seat, session_018HrVaotisyhgmot9o2MLRq, 2026-09-19T10:13Z. ⛔ No grade, no
    domain:*, no assignee changed. This card was this seat's next pick — it is the oldest p2 product
    card
    in the lane — and it is being blocked rather than dispatched, for a reason this card's own
    body predicted.

    The first gate is genuinely open

    reading all rows read 2026-09-19T10:13Z on objectstack origin/main and the published spec tags value
    objectstack#11513 — the server half this card named closed completed; refusal live
    objectstack#11703 — clone_permission_set copied 2 of 6 facets closed completed
    objectstack#11843 — the OS_METADATA_WRITABLE metadata-door probe closed completed
    NOT_OVERRIDABLE in @objectstack/spec@17.4.0 / 17.3.0, packages/spec/src 7 / 7
    CONTROL — package_id, same corpus, same query form 18 / 18

    ⇒ the refusal code is a declared member of the error-code ledger and installable at the version
    objectui resolves. ⛔ 「upstream merged」 is not the criterion and it is not what this row claims —
    this is the install surface, and it is satisfied.

    ⭐ The second gate: this card told its taker not to guess the predicate, and the obvious guess is wrong

    The body's own Sequencing section: 「the refusal shape and the provenance predicate it exposes decide
    what the UI keys the lock on — do not guess the predicate from the current row shape」.

    Measured at source, packages/plugins/plugin-security/src/security-plugin.ts:5591-:5604 — the
    admin-door gate keys on managed_by === 'package', and carries package_id only as nullable
    context on the refusal (:3753 records a publish that 「carried no owning package_id to stamp」).
    ⇒ a UI lock keyed on package_id presence — the guess the row shape invites — would miss
    package-managed rows whose package_id is null.

    ⚠️ Two further cautions found in the same read, and they are why this seat stopped:

    1. managed_by:'package' is ⛔ not the same predicate as 「the server will refuse this write」.
      :5544-:5551: update/delete are not refused at that gate at all — they are translated into
      a metadata write and refused by that producer, where a code-declared (artifact-backed) set
      draws 403 NOT_OVERRIDABLE while a sys_metadata-backed set 「rides allowRuntimeCreate and
      still lands」. ⇒ hard-locking every managed_by:'package' row shows a lock on a set the server
      would have accepted.
    2. permission-set-overlay-discard.ts states 「eligibility is NOT gated on managed_by」 for the
      discard path, and permission-set-projection.ts exists to control which columns a reader sees —
      so whether a UI reader even RECEIVES managed_by is itself unread.

    ⇒ Blocked on a reading, ⛔ not on a merge

    Filed as objectstack#19195 in the producing lane, carrying all of the above plus the three
    questions it asks (what the projection serves a non-system caller; which predicate the lock belongs
    on given that; whether the refusal is legible to a UI without string-matching prose). The body's
    Blocked-by: line is repointed to it in the same act — ⛔ the closed objectstack#11513 is not left
    there, because a satisfied Blocked-by: makes the unlock scan re-fire on a card that is not ready.

    ⛔ What this seat did NOT do: dispatch a dev to establish (1) or (3). They are runtime readings of a
    producer this seat cannot stand up, and a dev sent to guess them would produce exactly the lock this
    card's own body warns against. A zero-implementation stop carrying evidence is the better product
    here.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    Unblocked on the maintainer's word, with a narrowed scope · pm:blocked → pm:queue

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T02:33Z. ⛔ Not a claim, ⛔ not a dispatch.

    Provenance. The instruction is the maintainer's. The words, verbatim: 「同意」. Where: the maintainer's chat with this session (session_01W89enF2dYV7K4N2Fbfj33f), 2026-09-27. It agreed to this seat's recommendation A: unblock this card into pm:queue with the scope below. The two options not taken were B, reopen objectstack#19195 and wait for its reading, and C, leave the card blocked. objectstack#19195 stays closed.

    Why the blocker no longer holds

    • The target closed with no visible answer. objectstack#19195 was closed completed on 2026-09-20T10:42Z by os-sam, an account suspended since then. Its comments, and one on this card (4 counted, 3 visible), are hidden with the account, so the reading it asked for was never on the board.
    • The reading already lives in objectui's own code. On objectui origin/main, packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx locks a permission set on the artifact tier: isArtifactBackedLayer, the client mirror of the server's isArtifactBacked (objectui#4518 / objectui#4526). That is the predicate the server actually refuses on, 403 not_overridable for a code-declared set. It is ⛔ not managed_by: 'package', the guess 5740971177 warned would mis-lock a sys_metadata-backed package set. The editor already shows the provenance badge (platform / package / custom) and explains the refusal before Save.
    • The install surface is met for what remains. NOT_OVERRIDABLE is in @objectstack/spec@17.4.0 (measured in 5740971177). The clone_permission_set record action (label "Clone", list_item / record_header) is in packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts at the tag @objectstack/plugin-security@17.4.0 (read 2026-09-27T02:30Z), and the server's refusal names that action as the remedy.

    Scope now (what is left of the body's three items)

    1. Item 1 (locked, with a marker saying why) is delivered by the artifact tier above. ⛔ Keep that predicate, and do not add a second one.
    2. Item 2 is the work. In the locked editor, "Clone to customize" is the primary action, wired to the published clone_permission_set action. The locked-state guidance names cloning first. Today it offers "edit the source artifact and redeploy / create a new runtime set / OS_METADATA_WRITABLE" and never the ruled path, which was 「同意 第一步(创业阶段,Salesforce 式)」 on objectstack#11513: lock the base, clone to customize.
    3. Item 3: check that a save which still reaches the server surfaces the same guidance, not a generic toast. Fix it only if it does not.

    Execution notes

    • First, measure on a booted showcase: one code-package permission set, and one sys_metadata-backed package set. The editor's lock must agree with the server's answer in both cases (the first refused 403 not_overridable, the second saves). If they disagree, stop and report here: the lock predicate would then be in question, and that goes back to the board, ⛔ not to a guess.
    • Pin: the locked editor renders "Clone to customize" as its primary action; the clone lands an org-owned set that the editor opens writable; an org-owned set shows no clone prompt (the control).
    • domain:ui · priority:p2 unchanged.
  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 6 — domain:ui execution seat 2
    Session: session_014mXUNuFomfj24w7s1pZzhN
    Branch: claude/issue-5987-permset-clone-to-customize
    Worktree: objectui-issue-5987
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx (the locked editor: "Clone to customize" is its primary action, wired to the published clone_permission_set record action, and the locked-state guidance names cloning first; plus item 3, the refused-save guidance, only if a server refusal still reaches a generic toast); the i18n rows those strings need; tests; one changeset ('@object-ui/app-shell': patch). ⛔ The lock predicate stays isArtifactBackedLayer (artifact tier); no second predicate. Stop on breach and explain it in the report
    Container & model: S, mode:subagent, model: fable (the ceiling; opus subagents hit a weekly usage limit); executes the unblock triage 5851995299 (p2, the narrowed scope, on the maintainer's 「同意」)
    Clause-②: no — a Studio editor's action and guidance; no declared surface moves
    Thread-read: 5851995299
    Serial constraints cleared: open-PR file lists read 2026-09-27T04:18Z: no open PR touches PermissionMatrixEditor.tsx or any permission-set view. check-governed-queue-guard.mjs --test: NOT GOVERNED.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 5987,
    "status": "done",
    "branch": "claude/issue-5987-permset-clone-to-customize",
    "pr": "#10754",
    "session": "session_014mXUNuFomfj24w7s1pZzhN",
    "premise_still_valid": true,
    "summary": "The locked permission matrix (artifact tier, isArtifactBackedLayer unchanged) now renders 'Clone to customize' as its primary action in the slot Save would occupy, naming cloning first in its guidance (en-US + zh-CN). The button runs the server-published clone_permission_set record action on sys_permission_set through the console's shared action runner (useAction/apiHandler), resolved by name off the object definition the metadata store holds, with the set's row under params._rowRecord and the declared params surfaced to the dialog; no clone is hand-rolled, and a missing action or row is a named refusal. The returned clone is opened writable (../NAME) on the routed admin, or announced by name when embedded. Item 3: a save that reaches the env door and is refused 403 NOT_OVERRIDABLE now shows the same guidance and action keyed on the code, not the transport's message.",
    "tests": "Under the shared verify lock: new pin PermissionMatrixEditor.cloneToCustomize.test.tsx — red on base (base editor + base i18n, test file kept: Tests 11 failed | 6 passed (17), the 6 being absence-asserting controls) and green on head (17 passed). Seven per-hunk ablations, each landed on disk (node scripts proving anchor 1 to 0 + replacement present), run, restored via git checkout HEAD -- ABS_PATH proven by empty git diff HEAD and blob-hash equal to HEAD: A header button removed then 8 failed; B item-3 branch then 2 failed; C en-hint ordering reversed then 1 failed; D visible guidance strip then 2 failed; E zh perm.clone.action row deleted then 1 failed + check-i18n-designer-table-parity exit 1 naming perm.clone.action; F declared params not surfaced then 2 failed; G clone not opened then 1 failed. Full metadata-admin pin set: Test Files 22 passed (22) · Tests 119 passed (119). turbo build --filter=@object-ui/app-shell^... : Tasks 28 successful, 28 total. pnpm --filter @object-ui/app-shell type-check (script name echoed): VERDICT command-exit 0, no error TS. eslint narrowed to 3 changed files (lock-free, --format json counts 3 files): 0 errors, 32 warnings (31 pre-existing on unchanged lines + one catch (err: any) matching doSave's existing spelling); population is eslint . from packages/app-shell under the root flat config, which sets no parserOptions.project/projectService, so type-aware linting is off and this diff moves no untouched file's verdict. Lock-free gates all exit 0: check-i18n-designer-table-parity, check-i18n-en-drift, check-i18n-call-site-keys, check-changeset-presence (2 source files of 1 released package, 1 changeset added), check-changeset-no-major/-fixed/-overwrite/-claims, check-pending-changeset-literals, check-control-bytes, check-new-cross-file-line-citations (0 new), check-vi-mock-specifiers/-inherit/-override-shape, check-test-path-roots, check-unreferenced-sources, check-handler-key-read-sites, check-object-metadata-write-doors. CI owns: pnpm lint (repo scan), the test shards, pnpm check (needs the CLI build). H1 boot NOT MEASURED (objectstack node_modules empty, no dist, no runtime plugin installed in this container); source-level reading on objectstack origin/main 2bbebf5321 (sys-permission-set.object.ts byte-identical at tag @objectstack/plugin-security@17.4.0) shows client isArtifactBackedLayer and server isArtifactBacked agree on both a code-package set (both lock/refuse) and a sys_metadata-backed package set (both accept). What clone_permission_set returns NOT MEASURED live; by source it is the spec's CreateDataResponse {object,id,record} and the api handler unwraps to result.data.",
    "mcp_calls": "0 — no MCP GitHub tool used; issue/PR/comment reads were curl REST through the proxy, writes were the scripts/pm relay.",
    "api_writes": "3 REST proxy writes via the fleet-write relay: POST /repos/objectstack-ai/objectui/pulls (draft PR #10754); POST /repos//issues/10754/assignees (os-elon-musk, read back and matched); POST /repos//issues/5987/comments (this report). git push is not a REST write.",
    "open_questions": [],
    "out_of_scope_findings": ["class: b · reach: exception:release-text · the server's metadata-door refusal for a packaged permission set (saveMetaItem's artifact-tier 403 NOT_OVERRIDABLE in packages/metadata-protocol/src/protocol.ts) names 'edit the source artifact and redeploy' and 'OS_METADATA_WRITABLE' but NOT the clone remedy, unlike the data door's PackagedPermissionSetLockedError which names the Clone action; a Studio save (PUT /api/v1/meta/permission/:name via MetadataClient.save) hits the metadata door, so its message alone would be the pre-ruling guidance — which is exactly why this PR keys item 3 on the error code rather than the prose. objectstack-side, in @objectstack/plugin-security / metadata-protocol. carrier: none · dedupe words: NOT_OVERRIDABLE metadata-door refusal clone remedy permission set"]
    }

  8. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 5987,
    "addendum": true,
    "supersedes_comment": 5852878582,
    "status": "done",
    "branch": "claude/issue-5987-permset-clone-to-customize",
    "pr": "#10754",
    "head": "48aaf0f068",
    "session": "session_014mXUNuFomfj24w7s1pZzhN",
    "premise_still_valid": true,
    "regression": "CI Test (shard 1/8), job 108555660643, on 6986574: permission-slice.authoredKeys.test.ts 'carries every facet the editor can author' failed with expected [ 'objectName', 'params' ] to deeply equal []. Not in my local run: the metadata-admin pin set I ran did not include that file.",
    "cause": "Confirmed by replicating the test's scanner per setDraft( call (scratch script, not committed). The scanner reads the top-level keys of every object literal that opens with a spread inside a setDraft( balanced region of PermissionMatrixEditor.tsx / PermissionAdvancedFacets.tsx. Its CALL regex also matches the prose 'funnels through setDraft (matrix checkboxes, header inputs, capabilities,' inside a // comment above isDirty; balanced() starts at that '(' and the ')' that would close it sits on the NEXT comment line, which skipComment skips wholesale, so the region never closes and runs to end of file — on main too (base: region from line 617 to EOF, keys [objects, fields, name, label, systemPermissions], all carried). My dispatch literal { ...localizeActionTexts(...), objectName, params } is the action runner's call shape (DeclaredActionsBar's), not a permission-set facet, and it landed inside that runaway region, so its two keys were read as facets the slice drops. objectName and params are NOT new facets: nothing writes them into the draft and mergePermissionSlice never sees them.",
    "fix": "Option: move the action invocation's construction into a small helper outside the scanned surface — packages/app-shell/src/views/metadata-admin/permission-set-clone-dispatch.ts exporting PERMISSION_SET_OBJECT, CLONE_PERMISSION_SET_ACTION, findCloneAction(objectDefs) and buildCloneDispatch(cloneAction, row, localize). PermissionMatrixEditor.tsx now calls findCloneAction and buildCloneDispatch; no behaviour change (the 17 pins pass unchanged). Chosen over reshaping the literal because reordering the spread would silently change precedence against a server action def and would be dodging the scanner rather than stating that these keys are not facets; the helper's header states it. EDITOR_AUTHORED_KEYS untouched, no allow-list entry (the test's header prescribes none for non-facet keys; its SCAN_FLOOR is only the must-find list). The scanner's comment-matching is not changed here (out of scope, reported below). Scanner replica on the new head: keys [objects, fields, name, label, systemPermissions] — identical to base; zero leading-spread literals remain in my editor diff.",
    "tests": "All under the shared verify lock on head 48aaf0f after a closure rebuild (turbo build --filter=@object-ui/app-shell^... : Tasks 28 successful, 28 total; the re-created worktree had no dist, which made a first type-check answer TS2307 for every @object-ui import — a stale-build false red, discarded). permission-slice.authoredKeys.test.ts: Test Files 1 passed (1) · Tests 3 passed (3). Whole packages/app-shell/src/views/metadata-admin/ directory: Test Files 314 passed (314) · Tests 3629 passed | 1 skipped (3630) · VERDICT command-exit 0 (held 9m36s). PermissionMatrixEditor.cloneToCustomize.test.tsx: Test Files 1 passed (1) · Tests 17 passed (17). pnpm --filter @object-ui/app-shell type-check (script name echoed): VERDICT command-exit 0, zero 'error TS'. Lock-free on the new head, all exit 0: eslint on the editor + helper (0 errors; helper 0 warnings), check-changeset-presence (4 source files of 1 released package, 1 changeset declared), check-unreferenced-sources (helper reachable), check-control-bytes, check-new-cross-file-line-citations (0 new), check-i18n-designer-table-parity, check-vi-mock-specifiers. Serial: origin/main fetched to a private ref before the push; none of my five paths touched on main; git merge-tree --write-tree exit 0.",
    "mcp_calls": "0 — no MCP GitHub tool used in this round either.",
    "api_writes": "1 additional REST proxy write via the relay: POST /repos//issues/5987/comments (this addendum). Running total 4: POST /pulls, POST /issues/10754/assignees, two POST /issues/5987/comments. git push is not a REST write.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door + wrong answer — CI job 108555660643 red on keys that are not permission-set facets · permission-slice.authoredKeys.test.ts's scanDraftAuthoredKeys matches its CALL regex inside comments: the prose 'funnels through setDraft (matrix checkboxes, …' in PermissionMatrixEditor.tsx opens a balanced region whose ')' is on the next // line and is skipped, so the region runs to EOF on main (replica: base line 617 to EOF, 42832 chars) and every leading-spread literal in the rest of the file is read as an authored facet — the test's own header says the scan reads literals inside setDraft(...) calls. Today it is green on main only because every such literal below that line happens to carry a carried key. Fix belongs in the scanner (skip comments before matching CALL, or refuse an unterminated region as the header's anti-blind floor already refuses an empty scan). carrier: none · dedupe words: authoredKeys scanner setDraft comment unterminated region"
    ],
    "pr_body_replacement": "Fixes #5987\nClause-②: no\n\nPlaceholders in this body are written as capitalised words (NAME, PR_NUMBER) rather than in angle brackets, because the body sanitizer strips angle-bracket shapes at rest.\n\n## What changed\n\npackages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx, i18n.ts (new rows only, plus one rewritten hint), a small helper permission-set-clone-dispatch.ts (resolves the published action off the object definitions and shapes the runner dispatch), one new pin file, one changeset (@object-ui/app-shell: patch). The lock predicate isArtifactBackedLayer is byte-identical to main; no second predicate.\n\nWhy the helper (second commit): permission-slice.authoredKeys.test.ts scans PermissionMatrixEditor.tsx for { ...spread, key } literals inside setDraft( regions and reads their keys as permission-set facets. Its setDraft( regex also matches the prose funnels through setDraft (matrix checkboxes, … in a // comment, and the ) that would close that region sits on the next comment line, which the walker skips wholesale — so on main too that region runs to end of file and every leading-spread literal below it is counted. My dispatch literal ({ ...localized, objectName, params }, the action call's argument shape, not a facet) landed inside it and CI read objectName / params as dropped facets. Moving the dispatch construction into the helper, which the scan does not read, is the honest fix: the keys were never facets, and the slice's carried-key list is untouched. The scanner defect itself is reported in the dev report, not changed here.\n\n- The locked editor (artifact tier is the deciding gate: lockedByArtifactTier) renders Clone to customize in the header slot Save would occupy, and a visible guidance line (role=\"note\") that names cloning first. The badge hint perm.readOnly.artifact.hint is rewritten so cloning comes first and the former remedies (edit the source artifact and redeploy, a new runtime set, OS_METADATA_WRITABLE) follow as secondary routes; the server's reason and code stay in it.\n- Clicking it runs the server-published clone_permission_set record action on sys_permission_set through the console's shared action runner (useAction()), resolved by name off the object definition the console metadata store holds (useMetadata().ensureType('object') then findCloneAction), with the set's sys_permission_set row (found by name, the same lookup AssignedUsersSection uses) stashed under params._rowRecord and the declared params surfaced as actionParams (buildCloneDispatch) — the dispatch shape of DeclaredActionsBar. No clone is assembled from create/update calls: an object that publishes no such action, or a set with no row, is a named refusal (perm.clone.actionMissing, perm.clone.rowMissing).\n- On success the routed metadata admin navigates to ../NAME (relative: 'path', the spelling ResourceEditPage uses after create), where the clone loads with no code layer and is writable. An embedded host has no route and gets a role=\"status\" notice naming the clone.\n- Item 3: a save that still reaches the environment door and is refused 403 with code NOT_OVERRIDABLE (matched with errorCodeIs, never on prose) renders perm.save.locked with the same Clone button and keeps the server's sentence underneath, instead of the transport's message alone. Env door only (!packageId).\n\n## H1 — the agreement measurement\n\nBooted showcase: NOT MEASURED. Blocker, measured in this container: /home/user/objectstack has node_modules with 0 entries and no dist under packages/spec, packages/plugins/plugin-security or packages/cli; objectui's node_modules/@objectstack holds only spec (17.4.0), no runtime plugin. A showcase boot needs a full objectstack install plus a workspace build under the shared verify lock, which is outside this card's objectui-only file surface and the foreground cap. Not guessed.\n\nSource-level reading instead (objectstack origin/main at 2bbebf5321; sys-permission-set.object.ts is byte-identical at tag @objectstack/plugin-security@17.4.0, git diff empty), on the same two inputs:\n\n- Server isArtifactBacked(type, name) in metadata-protocol/src/protocol.ts is true when lookupArtifactItem(type, name) answers — artifact items only, the sys_metadata rehydration sentinel excluded, plain-key shadows ignored — or when isNestedArtifactField does (declared field-only, irrelevant to permission).\n- The layered read's code layer is the MetadataService copy when one exists, else lookupArtifactItem(...) ?? registry.getItem(...); a DB-hydrated runtime shadow carries _packageId: 'sys_metadata'. Its provenance is the item's own _provenance stamp (resolveLockState → extractProtection, @objectstack/spec kernel metadata-protection.zod.ts).\n- Client isArtifactBackedLayer(lay) is code non-null AND code._packageId !== 'sys_metadata' AND provenance !== 'org'.\n- Case A, a code-package set: server true → saveMetaItem refuses the env-door PUT with 403 NOT_OVERRIDABLE (allowOrgOverride: false, when environmentId is defined); client code carries the package id → locked. Agree.\n- Case B, a sys_metadata-backed package set (managed_by: 'package', definition only in sys_metadata): server lookupArtifactItem excludes the sentinel → false → the allowRuntimeCreate tier accepts; client code is the hydrated shadow with _packageId: 'sys_metadata' (or null) → not locked. Agree.\n- The data door's classifyPackagedPermissionSet (packaged-permission-set-lock.ts) decides from the same artifact rule (declaredPackageIdOf excludes _envProjection echoes and the sentinel), so all three spellings key on one fact.\n- Known residue, unchanged (objectui#4526): on a single-kernel host (environmentId undefined) the server disengages its artifact tier and accepts; the client still locks. Conservative, previously accepted.\n\n## H2 — how the console invokes a published record action\n\nclone_permission_set is type: 'api', method: 'POST', target: '/api/v1/data/sys_permission_set', bodyExtra: { active: true }, params: label and name (required text), description and the five facets with defaultFromRow / carryOver. It is not a script action, so serverActionHandler is not its path: the console dispatches type: 'api' through useConsoleActionRuntime.apiHandler, registered on the ActionProvider that GlobalActionRuntimeProvider (ConsoleShell) wraps every route in, with onParamCollection seeding defaultFromRow params from params._rowRecord. The editor uses that same provider via useAction().\n\nWhat the action returns: NOT MEASURED on a live server. By source, the data door answers the spec's CreateDataResponse — { object, id, record } — with status 201 (runtime/src/domains/data.ts, CreateDataResponseSchema in the installed @objectstack/spec@17.4.0), and apiHandler unwraps the { success, data } envelope to result.data. The editor reads result.data.record.name; when that is absent it says so (perm.clone.noName) rather than guessing.\n\n## H3 — strings\n\nAll through metadata-admin/i18n.ts, en-US and zh-CN rows. Rewritten: perm.readOnly.artifact.hint. New: perm.clone.action, perm.clone.hint, perm.clone.guidance, perm.clone.actionMissing, perm.clone.rowMissing, perm.clone.noName, perm.clone.done, perm.save.locked. check:i18n-designer-parity and check:i18n-drift (designer population) both green.\n\n## H4 — item 3\n\nMeasured with the 403 mocked at the data-source boundary (client.save rejecting with status: 403, code: 'NOT_OVERRIDABLE'). Before: the red strip rendered the transport's message. For the metadata door that message (saveMetaItem's artifact-tier refusal) says to edit the source artifact and redeploy or set OS_METADATA_WRITABLE — the pre-ruling remedies, never the clone — so it was the generic form. Changed as described above; the pin and its controls are below.\n\n## Pins and ablations\n\nNew file PermissionMatrixEditor.cloneToCustomize.test.tsx, 17 tests, real ActionProvider with the runtime's three seams faked (api handler, param dialog, toast); adapter and metadata store faked through @object-ui/react with importOriginal spread.\n\nRed on the base editor plus base i18n (a2f361b8c1, the test file kept): Tests 11 failed | 6 passed (17) — the 6 that pass on base are the controls (org-owned / sentinel / type-locked / host-locked show no clone prompt; other refusals stay generic), which hold on base by construction. Head: 17 passed.\n\nPer-hunk ablations, each landed on disk (anchor 1 → 0, replacement present), run, and restored with git checkout HEAD -- ABSOLUTE_PATH, proven by git diff HEAD empty and blob hash equal to HEAD's:\n\n- A header Clone button removed → 8 failed (primary action, zh, dispatch, opens writable, embedded notice, cancel, both no-fallback refusals).\n- B item-3 branch removed → 2 failed (NOT_OVERRIDABLE guidance, lowercase-code spelling).\n- C en hint with the secondary routes moved before cloning → 1 failed (ordering pin).\n- D visible guidance strip removed → 2 failed (primary-action pin, zh).\n- E zh perm.clone.action row deleted → 1 failed (zh), and check-i18n-designer-table-parity exit 1 naming perm.clone.action.\n- F declared params not surfaced to the runner → 2 failed (dispatch pin, cancel pin).\n- G clone not opened after success → 1 failed (opens-writable pin).\n\n## Gates (local; the farm is CI's)\n\nAll under the shared verify lock unless noted; verdict lines quoted from the tools.\n\n- pnpm exec vitest run --maxWorkers=2 over PermissionMatrixEditor*, the metadata-admin i18n pins and PackageOwdOverviewPanel.readDecorationStrip: Test Files 22 passed (22) · Tests 119 passed (119) after the one selector fix (first run 118/119; the zh pin matched two elements).\n- pnpm exec turbo run build --filter=@object-ui/app-shell^... --concurrency=2: Tasks: 28 successful, 28 total.\n- pnpm --filter @object-ui/app-shell type-check (script name echoed): VERDICT command-exit 0, no error TS.\n- eslint, narrowed to the three changed files (lock-free): 0 errors, 32 warnings; population = eslint . from packages/app-shell under the root flat config; --format json counts 3 files; the config sets no parserOptions.project / projectService, so type-aware linting is off and this diff cannot move any untouched file's verdict. Warnings: 31 pre-existing on unchanged lines plus one catch (err: any) in the new handler, the spelling doSave already uses. pnpm lint (repo scan) is CI's.\n- Lock-free gates, each exit 0: check-i18n-designer-table-parity, check-i18n-en-drift, check-i18n-call-site-keys, check-changeset-presence (2 source file(s) of 1 released package(s) changed ... declares 1 changeset(s)), check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-changeset-claims, check-pending-changeset-literals, check-control-bytes, check-new-cross-file-line-citations (0 new citation(s)), check-vi-mock-specifiers, check-vi-mock-inherit, check-vi-mock-override-shape, check-test-path-roots, check-unreferenced-sources, check-handler-key-read-sites, check-object-metadata-write-doors.\n- Not run locally, CI's: pnpm lint, the four/eight test shards, pnpm check (needs the CLI build).\n\n## Serial\n\norigin/main fetched into a private ref (refs/issue5987/main, 01700dcd7d) before the final push: 1 commit since the base a2f361b8c1, none touching the changed paths; git merge-tree --write-tree against HEAD exits 0. No merge commit needed.\n\n## Acceptance notes\n\n- Observation (objectstack side, not in this PR): the metadata door's artifact-tier refusal text in saveMetaItem still names edit-and-redeploy and OS_METADATA_WRITABLE, while the data door's PackagedPermissionSetLockedError names the Clone action. The Studio editor hits the metadata door, which is why item 3 had to key on the code. Reported in the dev report for the seat, not filed here.\n- The 6 control pins pass on base by construction (they assert an absence); the 11 affordance pins are the red-then-green evidence.\n- useAction() falls back to a bare ActionRunner when no ActionProvider is mounted (a host that embeds the editor outside ConsoleShell); that fallback is the platform's existing shape for every useAction() consumer and is not changed here.\n- If the typed clone name collides with a package-declared set, the data door's insert leg refuses it; that refusal is the published action's own and reaches the user through the runner's error toast, unchanged.\n- The single-kernel residue of objectui#4526 is unchanged.\n\n---\n_Generated by Claude Code_\n"
    }

  9. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 5987,
    "round": 2,
    "supersedes_comments": [
    5852878582,
    5853090672
    ],
    "status": "done",
    "branch": "claude/issue-5987-permset-clone-to-customize",
    "pr": "#10754",
    "head": "2f47d40422",
    "session": "session_014mXUNuFomfj24w7s1pZzhN",
    "premise_still_valid": true,
    "summary": "Round 2, published text only, per the contract review record (comment 5853251945, FAIL at 48aaf0f on text). One commit 2f47d40, one file .changeset/5987-permset-clone-to-customize.md (md5 796364a5879f561ed0748f29b4eee335 before, 31bb4262ee708eab72e78455936b16a9 after): (a) the CHANGELOG-bound sentence now reads 'was never on the screen, and the metadata-door refusal a Studio save receives names only the pre-ruling remedies; the data door's refusal is the one that names the Clone action.' (b) frontmatter '@object-ui/app-shell': patch changed to minor — the seat's correction of its own order (new user-facing affordance = feature under .changeset/README.md, matching the feat( headline and sibling objectui#10449). No source change: git show --stat lists 1 file, 4 insertions, 2 deletions. pr_body_replacement carries the record's replacements 1–5, the minor note, and a Round 2 section.",
    "tests": "Text-only commit, so per the order no suite re-run beyond the changeset and text gates. On the worktree at 2f47d40, each exit 0 with the tool's own line: check-changeset-fixed ('privatePackages declared: version=true, tag=false'); check-changeset-no-major ('No changeset declares a major bump') — the two are changeset:check; check-changeset-presence ('4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'); check-changeset-overwrite ('No pre-existing changeset was modified or deleted' — this changeset is new on the branch, so the edit is not an overwrite case); check-changeset-claims; check-control-bytes ('OK (scanned 8933 tracked text file(s))'); check-new-cross-file-line-citations ('0 new citation(s)'). eslint re-measured at 2f47d40 on PermissionMatrixEditor.tsx and permission-set-clone-dispatch.ts (--format json, 2 files): editor 0 errors / 21 warnings (pre-existing, unchanged lines), helper 0 / 0 — a first attempt answered exit 2 with empty output because the re-created worktree had no node_modules; that is a non-measurement and was discarded, then pnpm install and the run above. Pins, ablations, type-check and the whole-directory run (314 files, 3629 passed, 1 skipped) hold as measured at 48aaf0f since no source moved. Serial re-measured at the push: origin/main fetched to a private ref = 704e05b; git rev-list --count a2f361b..main = 6 (--first-parent 6, --merges 0) — the record said 7; the six are 01700dc, e2b8f99, 9327397, 7b395d8, 1422a92, 704e05b; none of my five paths touched on main; git merge-tree --write-tree exit 0. Pushed 48aaf0f..2f47d40; remote head equals local.",
    "mcp_calls": "0 — no MCP GitHub tool used; PR comments read via curl REST, the write via the scripts/pm relay.",
    "api_writes": "1 additional REST proxy write via the relay: POST /repos//issues/5987/comments (this report). Running total 5: POST /pulls, POST /issues/10754/assignees, three POST /issues/5987/comments. No PATCH of the PR body. git push is not a REST write.",
    "deviations": [
    "Serial count reported as measured (6 commits since base by rev-list --count, no merges) rather than the record's 7; the pr_body_replacement says so and the six commits are listed above.",
    "In the H1 Case B sentence the record's client-half replacement was applied verbatim and the server half was aligned to the record's own ① reading (getArtifactItem misses a DB-hydrated row) because the old server half stated the same wrong sentinel mechanism; noted in the body's Round 2 section."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: the seat (per the coordinator, the authoredKeys scanner defect is confirmed and the seat files it; dev does nothing further) · noted, not filed"
    ],
    "pr_body_replacement": "Fixes #5987\nClause-②: no\n\nPlaceholders in this body are written as capitalised words (NAME, PR_NUMBER) rather than in angle brackets, because the body sanitizer strips angle-bracket shapes at rest.\n\n## What changed\n\npackages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx, i18n.ts (new rows only, plus one rewritten hint), a small helper permission-set-clone-dispatch.ts (resolves the published action off the object definitions and shapes the runner dispatch), one new pin file, one changeset (@object-ui/app-shell: minor — the dispatch order said patch; the seat corrected its own order in the contract review at 48aaf0f068: a new user-facing affordance is a feature under .changeset/README.md, matching the feat( headline and sibling objectui#10449). The lock predicate isArtifactBackedLayer is byte-identical to main; no second predicate.\n\nWhy the helper (second commit): permission-slice.authoredKeys.test.ts scans PermissionMatrixEditor.tsx for { ...spread, key } literals inside setDraft( regions and reads their keys as permission-set facets. Its setDraft( regex also matches the prose funnels through setDraft (matrix checkboxes, … in a // comment, and the ) that would close that region sits on the next comment line, which the walker skips wholesale — so on main too that region runs to end of file and every leading-spread literal below it is counted. My dispatch literal ({ ...localized, objectName, params }, the action call's argument shape, not a facet) landed inside it and CI read objectName / params as dropped facets. Moving the dispatch construction into the helper, which the scan does not read, is the honest fix: the keys were never facets, and the slice's carried-key list is untouched. The scanner defect itself is reported in the dev report, not changed here.\n\n- The locked editor (artifact tier is the deciding gate: lockedByArtifactTier) renders Clone to customize in the header slot Save would occupy, and a visible guidance line (role=\"note\") that names cloning first. The badge hint perm.readOnly.artifact.hint is rewritten so cloning comes first and the former remedies (edit the source artifact and redeploy, a new runtime set, OS_METADATA_WRITABLE) follow as secondary routes; the server's reason and code stay in it.\n- Clicking it runs the server-published clone_permission_set record action on sys_permission_set through the console's shared action runner (useAction()), resolved by name off the object definition the console metadata store holds (useMetadata().ensureType('object') then findCloneAction), with the set's sys_permission_set row (found by name, the same lookup AssignedUsersSection uses) stashed under params._rowRecord and the declared params surfaced as actionParams (buildCloneDispatch) — the dispatch shape of DeclaredActionsBar. No clone is assembled from create/update calls: an object that publishes no such action, or a set with no row, is a named refusal (perm.clone.actionMissing, perm.clone.rowMissing).\n- On success the routed metadata admin navigates to ../NAME (relative: 'path', the spelling ResourceEditPage uses after create), where the clone loads with no code layer and is writable. An embedded host has no route and gets a role=\"status\" notice naming the clone.\n- Item 3: a save that still reaches the environment door and is refused 403 with code NOT_OVERRIDABLE (matched with errorCodeIs, never on prose) renders perm.save.locked with the same Clone button and keeps the server's sentence underneath, instead of the transport's message alone. Env door only (!packageId).\n\n## H1 — the agreement measurement\n\nBooted showcase: NOT MEASURED. Blocker, measured in this container: /home/user/objectstack has node_modules with 0 entries and no dist under packages/spec, packages/plugins/plugin-security or packages/cli; objectui's node_modules/@objectstack holds only spec (17.4.0), no runtime plugin. A showcase boot needs a full objectstack install plus a workspace build under the shared verify lock, which is outside this card's objectui-only file surface and the foreground cap. Not guessed.\n\nSource-level reading instead (objectstack origin/main at 2bbebf5321; sys-permission-set.object.ts is byte-identical at tag @objectstack/plugin-security@17.4.0, git diff empty), on the same two inputs:\n\n- Server isArtifactBacked(type, name) in metadata-protocol/src/protocol.ts is true when lookupArtifactItem(type, name) answers — artifact items only, the sys_metadata rehydration sentinel excluded, plain-key shadows ignored — or when isNestedArtifactField does (declared field-only, irrelevant to permission).\n- The layered read's code layer is the MetadataService copy when one exists, else lookupArtifactItem(...) ?? registry.getItem(...); a DB-hydrated runtime shadow carries _packageId: 'sys_metadata'. Its provenance is the item's own _provenance stamp (resolveLockState → extractProtection, @objectstack/spec kernel metadata-protection.zod.ts).\n- Client isArtifactBackedLayer(lay) is code non-null AND code._packageId !== 'sys_metadata' AND provenance !== 'org'.\n- Case A, a code-package set: server true → saveMetaItem refuses the env-door PUT with 403 NOT_OVERRIDABLE (allowOrgOverride: false, when environmentId is defined); client code carries the package id → locked. Agree.\n- Case B, a sys_metadata-backed package set (managed_by: 'package', definition only in sys_metadata): server lookupArtifactItem (getArtifactItem, artifact-only) misses a DB-hydrated row → false → the allowRuntimeCreate tier accepts; client code is the hydrated item, registered with no _packageId and stamped _provenance: 'org' (hydrateOverlayIntoRegistry then stateTenantAuthorship), so provenance !== 'org' is false and the editor does not lock; a just-saved row carries the sys_metadata sentinel until reboot. Agree.\n- The data door's classifyPackagedPermissionSet (packaged-permission-set-lock.ts) decides from the same artifact rule (declaredPackageIdOf excludes _envProjection echoes and the sentinel), so all three spellings key on one fact.\n- Known residue, unchanged (objectui#4526): on a single-kernel host (environmentId undefined) the server disengages its artifact tier and accepts; the client still locks. Conservative, previously accepted.\n\n## H2 — how the console invokes a published record action\n\nclone_permission_set is type: 'api', method: 'POST', target: '/api/v1/data/sys_permission_set', bodyExtra: { active: true }, params: label and name (required text), description and the five facets with defaultFromRow / carryOver. It is not a script action, so serverActionHandler is not its path: the console dispatches type: 'api' through useConsoleActionRuntime.apiHandler, registered on the ActionProvider that GlobalActionRuntimeProvider (ConsoleShell) wraps every route in, with onParamCollection seeding defaultFromRow params from params._rowRecord. The editor uses that same provider via useAction().\n\nWhat the action returns: NOT MEASURED on a live server. By source, the data door answers the spec's CreateDataResponse — { object, id, record } — with status 201 (runtime/src/domains/data.ts, CreateDataResponseSchema in the installed @objectstack/spec@17.4.0), and apiHandler unwraps the { success, data } envelope to result.data. The editor reads result.data.record.name; when that is absent it says so (perm.clone.noName) rather than guessing.\n\n## H3 — strings\n\nAll through metadata-admin/i18n.ts, en-US and zh-CN rows. Rewritten: perm.readOnly.artifact.hint. New: perm.clone.action, perm.clone.hint, perm.clone.guidance, perm.clone.actionMissing, perm.clone.rowMissing, perm.clone.noName, perm.clone.done, perm.save.locked. check:i18n-designer-parity and check:i18n-drift (designer population) both green.\n\n## H4 — item 3\n\nMeasured with the 403 mocked at the data-source boundary (client.save rejecting with status: 403, code: 'NOT_OVERRIDABLE'). Before: the red strip rendered the transport's message. For the metadata door that message (saveMetaItem's artifact-tier refusal) says to edit the source artifact and redeploy or set OS_METADATA_WRITABLE — the pre-ruling remedies, never the clone — so it was the generic form. Changed as described above; the pin and its controls are below.\n\n## Pins and ablations\n\nNew file PermissionMatrixEditor.cloneToCustomize.test.tsx, 17 tests, real ActionProvider with the runtime's three seams faked (api handler, param dialog, toast); adapter and metadata store faked through @object-ui/react with importOriginal spread.\n\nRed on the base editor plus base i18n (a2f361b8c1, the test file kept): Tests 11 failed | 6 passed (17) — the 6 that pass on base are the controls (org-owned / sentinel / type-locked / host-locked show no clone prompt; other refusals stay generic), which hold on base by construction. Head: 17 passed.\n\nPer-hunk ablations, each landed on disk (anchor 1 → 0, replacement present), run, and restored with git checkout HEAD -- ABSOLUTE_PATH, proven by git diff HEAD empty and blob hash equal to HEAD's:\n\n- A header Clone button removed → 8 failed (primary action, zh, dispatch, opens writable, embedded notice, cancel, both no-fallback refusals).\n- B item-3 branch removed → 2 failed (NOT_OVERRIDABLE guidance, lowercase-code spelling).\n- C en hint with the secondary routes moved before cloning → 1 failed (ordering pin).\n- D visible guidance strip removed → 2 failed (primary-action pin, zh).\n- E zh perm.clone.action row deleted → 1 failed (zh), and check-i18n-designer-table-parity exit 1 naming perm.clone.action.\n- F declared params not surfaced to the runner → 2 failed (dispatch pin, cancel pin).\n- G clone not opened after success → 1 failed (opens-writable pin).\n\n## Gates (local; the farm is CI's)\n\nAll under the shared verify lock unless noted; verdict lines quoted from the tools.\n\n- pnpm exec vitest run --maxWorkers=2 over PermissionMatrixEditor*, the metadata-admin i18n pins and PackageOwdOverviewPanel.readDecorationStrip: Test Files 22 passed (22) · Tests 119 passed (119) after the one selector fix (first run 118/119; the zh pin matched two elements).\n- Whole packages/app-shell/src/views/metadata-admin/ directory, every test file, at 48aaf0f068: Test Files 314 passed (314) · Tests 3629 passed | 1 skipped (3630) (VERDICT command-exit 0).\n- pnpm exec turbo run build --filter=@object-ui/app-shell^... --concurrency=2: Tasks: 28 successful, 28 total.\n- pnpm --filter @object-ui/app-shell type-check (script name echoed): VERDICT command-exit 0, no error TS.\n- eslint on the editor and the helper (lock-free, --format json, re-measured at 2f47d40422): 0 errors — editor 21 pre-existing warnings on unchanged lines, helper 0. Population = eslint . from packages/app-shell under the root flat config, which sets no parserOptions.project / projectService, so type-aware linting is off and this diff cannot move any untouched file's verdict. pnpm lint (repo scan) is CI's.\n- Lock-free gates, each exit 0: check-i18n-designer-table-parity, check-i18n-en-drift, check-i18n-call-site-keys, check-changeset-presence (4 source file(s) of 1 released package(s) changed ... declares 1 changeset(s)), check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check-changeset-claims, check-pending-changeset-literals, check-control-bytes, check-new-cross-file-line-citations (0 new citation(s)), check-vi-mock-specifiers, check-vi-mock-inherit, check-vi-mock-override-shape, check-test-path-roots, check-unreferenced-sources, check-handler-key-read-sites, check-object-metadata-write-doors.\n- Not run locally, CI's: pnpm lint, the four/eight test shards, pnpm check (needs the CLI build).\n\n## Serial\n\norigin/main fetched into a private ref before each push. At the final push (2f47d40422) origin/main is 704e05b09d: 6 commits since the base a2f361b8c1 by git rev-list --count (no merges; the review record counted 7 — the six are listed in the dev report), none touching the changed paths; git merge-tree --write-tree against HEAD exits 0. No merge commit needed.\n\n## Round 2 (published text only)\n\nHead 2f47d40422, commit docs(changeset): grade Clone to customize minor and name the door whose refusal lacks the clone remedy (objectui#5987), one file: .changeset/5987-permset-clone-to-customize.md (md5 796364a5879f561ed0748f29b4eee335 before, 31bb4262ee708eab72e78455936b16a9 after). No source change, so the pins, ablations, type-check and directory run above hold as measured at 48aaf0f068.\n\n- Frontmatter '@object-ui/app-shell': patch → minor (the seat's own correction; see "What changed").\n- The CHANGELOG-bound sentence now reads: "was never on the screen, and the metadata-door refusal a Studio save receives names only the pre-ruling remedies; the data door's refusal is the one that names the Clone action." It used to say the server's 403 refusal names the clone, which is true of the data door only.\n- Gates at this head, each exit 0: check-changeset-fixed, check-changeset-no-major (together changeset:check), check-changeset-presence (4 source file(s) of 1 released package(s) ... declares 1 changeset(s)), check-changeset-overwrite (No pre-existing changeset was modified or deleted — this changeset is new on this branch), check-changeset-claims, check-control-bytes, check-new-cross-file-line-citations (0 new citation(s)); eslint and Serial as re-measured above.\n- The H1 Case B sentence's server half was aligned to the record's reading (getArtifactItem misses a DB-hydrated row) alongside the verbatim client-half replacement.\n\n## Acceptance notes\n\n- Observation (objectstack side, not in this PR): the metadata door's artifact-tier refusal text in saveMetaItem still names edit-and-redeploy and OS_METADATA_WRITABLE, while the data door's PackagedPermissionSetLockedError names the Clone action. The Studio editor hits the metadata door, which is why item 3 had to key on the code. Reported in the dev report for the seat, not filed here.\n- The 6 control pins pass on base by construction (they assert an absence); the 11 affordance pins are the red-then-green evidence.\n- useAction() falls back to a bare ActionRunner when no ActionProvider is mounted (a host that embeds the editor outside ConsoleShell); that fallback is the platform's existing shape for every useAction() consumer and is not changed here.\n- If the typed clone name collides with a package-declared set, the data door's insert leg refuses it; that refusal is the published action's own and reaches the user through the runner's error toast, unchanged.\n- The single-kernel residue of objectui#4526 is unchanged.\n\n---\n_Generated by Claude Code_\n"
    }

  10. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: objectui#10754 at 2f47d40; the body, ready and auto-merge follow this ACCEPT

    domain:ui seat #2, session_014mXUNuFomfj24w7s1pZzhN. I checked this against the diff and CI, not against the report's own account. Claim 5852579635. It executes the unblock triage 5851995299 (p2, the narrowed scope, on the maintainer's 「同意」).

    Implemented-by:  claude/issue-5987-permset-clone-to-customize
    Reviewed-by:     session_014mXUNuFomfj24w7s1pZzhN
    
    item reading
    the change The lock. A permission set that the artifact tier locks (isArtifactBackedLayer, byte-identical to main; no second predicate) now renders Clone to customize in the Save slot. A visible guidance line names cloning first. The clone. The button runs the server-published clone_permission_set record action on sys_permission_set. It goes through the console's shared action runner (useAction()), with the set's row under _rowRecord and the declared params surfaced to the param dialog, which is DeclaredActionsBar's dispatch shape. No clone is hand-rolled. A missing action or a missing row is a named refusal. On success the routed admin opens the clone, which is writable. Item 3. A 403 NOT_OVERRIDABLE on the environment door is matched by code and renders the same Clone guidance
    the agreement measurement Source-level only, and stated as such. The booted showcase is NOT MEASURED because no objectstack install exists in this container. Cases A and B agree between server and client. The single-kernel residue of objectui#4526 is unchanged and stated
    pins 17 tests: 11 red on base, 17 green on head. Six are controls that hold on base by construction. Seven per-hunk ablations A–G each redden the rows they name. The whole metadata-admin/ directory is green: 314 files, 3629 tests
    the CI red on 6986574 permission-slice.authoredKeys.test.ts read the new dispatch literal as dropped facets. Root cause: that test's scanner runs to end of file from a comment on main. It was fixed honestly, by moving the dispatch construction into a helper the scan does not read. The scanner defect is filed as objectui#10764
    Clause-②: no / semver minor, the seat's ruling. The seat's order said patch; round 1 flagged the conflict with .changeset/README.md ("minor (new feature)") and the feat( headline; the seat then ruled minor, correcting its own order. The sibling objectui#10449 is minor
    reviews three rounds, all at CONTRACT_REVIEW_TIER: Round 1 FAIL at 48aaf0f (5853251945), one CHANGELOG-bound sentence and five body items · Round 2 FAIL at 2f47d40 (5853438249), three body items · Round 3 PASS at 2f47d40, body-only, posted with this landing. The seat read every transcript: every model turn ran at tier. Round 3 was interrupted once by an API session limit and resumed
    seat edits to the dev's body round 2's three replacements (R1–R3), verbatim. It also drops one clause round 2 could not locate on main: "a just-saved row carries the sys_metadata sentinel until reboot"
    serial git merge-tree --write-tree against main 25c7d584e4 is clean (tree c00fbe8e9d). None of the five paths moved on main
    governed NOT GOVERNED (0 of 5 paths)
    CI head 2f47d40: 40 success, 3 skipped, 0 failure
    closing keywords Fixes #5987 only

    Findings, each with its disposition

    • The authoredKeys scanner reads past a comment to end of file ⇒ objectui#10764.
    • Objectstack side, observation: the metadata door's artifact-tier refusal text (saveMetaItem) still names edit-and-redeploy and OS_METADATA_WRITABLE. The data door's refusal names the Clone action. Stated in the changeset and body; not carded here, since the text is the server's.
    • Under the package door (the Studio Access pillar), no clone affordance exists. This is consistent with the narrowed artifact-tier scope, not with the card title's broader wording. Recorded, not carded.
    • The bare useAction() fallback outside ConsoleShell would POST without the dialog. It is unreachable today; a useHasActionProvider() guard is a possible follow-up. Not carded.

    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

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions