You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Studio Access: creating a permission set whose derived id equals an already-published set puts a blank draft over the live set (publishing it would wipe its grants), and Studio offers no way to discard it #26836
Filing gate: ① product defect, reproduced, landing site named.
Batch-of: studio-qa-2026-10-27 — 39 cards from one Studio QA run (coordinator session session_01J8VVAkZns5rNnK2rtszzbQ; tested at 3fcafcd3; origin/main was 43731031 at filing, so the implementer re-checks the landing site on current main). The run record is the qa-run issue filed with this batch.
Symptom
The create dialog's first-write guard only checks for an existing DRAFT row, so a name that derives to a published id silently starts a blank replacement of a live set.
Reproduction
Publish project_editor_qa.
Access › "+" › Name "Project Editor (QA)" → Save as draft → toast "created"; GET /api/v1/meta/permission/project_editor_qa?state=draft → {objects:{},fields:{}}.
There is no discard control in Studio; publishing would replace the live set with an empty one. Coordinator API-confirmed.
Expected
Creation refuses an id that already exists in any state (offering "open existing"), and Studio can discard a draft.
Suspected landing site (the implementer verifies)
AccessPillar doCreate (ifNoneMatch: "*" guards only the draft row).
Filing facts
reach: Studio UI + metadata API, measured by the Access QA card and the coordinator.
Business reason: A routine "new set" can stage the removal of every grant of an existing set.
Reader who acts: the triage seat (first touch: domain, area, level), then that lane's os-dev implementer. QA suggestion only: domain:ui, area:access, p2.
p1: creating a permission set whose derived id equals a published set starts a blank draft over the live set, and publishing it would wipe its grants. The first-write guard checks the published row as well as the draft, and refuses with a plain message naming the existing set. If the metadata door also lets that write through, the door's half is a domain:server child. Re-check the landing site on current main.
Claim: PM loop round 1
Session: session_01QsR2jTfvjNTaR29ehLk26T
Account: zhuangjianguo
Branch: claude/project-thread-m5noqq (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
Worktree: objectstack-issue-26836
Domain: domain:ui
Seat: domain:ui#4
File surface: ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (AccessPillar only), studio namespace en.ts/zh.ts, tests beside, one root changeset; ⛔ packages/spec/**, server packages (a door half is reported as a domain:server child), any new key (stop on breach; explain in the report)
Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z), effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: Studio's Access create path (ui app-shell, this lane) writes a blank draft over a published permission set, because its first-write guard covers the draft row only · the platform path that may cover it: the metadata door's If-None-Match: *, which the thread measures (a server child if it should refuse a published row) · reached by every "+ New" permission set whose name derives to a live set's id
Thread-read: 6450910097
Premise: holds (AccessPillar.doCreate saves with mode: 'draft', ifNoneMatch: '*' and no published-row check, StudioDesignSurface.tsx ~8544-8558, on origin/main c4a4aefc at 2026-10-27T20:19Z)
Serial constraints cleared: open PR #26839 (#26705) touches AccessPillar's held-edit hook (~8473), a different region; PR #26824 touches the slot host only (scan of the 80 most recently updated open PRs 2026-10-27T20:20Z)
Dispatched at 2026-10-27T20:22Z to the card thread on that branch; the order lives in the thread's opening message.
{
"issue": 26836,
"status": "done",
"branch": "claude/project-thread-m5noqq",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26916",
"session": "session_01QsR2jRu5CpdGJFtUFzPvUy",
"premise_still_valid": true,
"summary": "AccessPillar.doCreate now reads the published permission sets and the draft headers across every package (client.list('permission') + client.listDrafts({ type: 'permission' }), the Interfaces create's unscoped pair) before writing; a taken identifier is refused in the dialog with a sentence naming the set, plus an 'Open LABEL' path when the set is on this package's rail. A fresh identifier saves as before (mode draft, packageId, ifNoneMatch '*', surface studio). An open set with a pending draft gets 'Discard draft' in its header: confirm, then client.reset('permission', NAME, { state: 'draft', packageId }) (the metadata editor's discard door, existing API); a published set re-reads its live body (matrix publishNonce + local nonce), a never-published set leaves the rail. Six new studio-namespace rows (en + zh). Door measured: no server child (see open_questions).",
"tests": "head 4dd29301. New StudioDesignSurface.accessCreateTaken-26836.test.tsx on the real AccessPillar + PermissionMatrixEditPage: Test Files 1 passed, Tests 9 passed (9), under os-verify-lock. Ablation via scripts/ablation-replace.mjs (anchor 'if (publishedRow || draftRow) {' -> 'if (false && (publishedRow || draftRow)) {', x1 -> x0): Tests 3 failed | 6 passed (the three refusal cases); restored, blob == HEAD b2086038, git diff HEAD empty; tests import the pillar from source, no build in between. app-shell + i18n type-check: VERDICT command-exit 0. Narrowed suites (66 app-shell files naming AccessPillar / studio namespace / engine.studio.access + all of packages/i18n/): Test Files 155 passed (155), Tests 2159 passed | 13 skipped. check:i18n-designer-parity exit 0; check:new-line-citations 0 new.",
"gates": "dispatch-gates --commands derived 34 at 4dd29301; all 34 exit 0 (PR-body gates against the final body via GITHUB_EVENT_PATH, re-run after the Verification section: closing-keyword-parity 0, adr-0087-registration 0, changeset-no-major 0, partof-closing-keyword 0). vitest run scripts/: 178 passed | 2 skipped files, 5065 passed tests. check:type-check-debt: 0 raw tsc errors. dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED. CI check-runs on #26916: in_progress at report time.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "4 — GitHub MCP issue_read x3 (get, get_comments x2) on #26836, all reads; claude-code-remote get_session x1. No MCP write.",
"api_writes": "2 — relay repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches x2 (pr_create #26916 with assignees [zhuangjianguo], run 40583946894, read-back identical 7927 bytes; this os-dev-report comment). git push is not a REST write.",
"line_budget": "5 files, +491 / -6 (dispatch-gates count at 4dd29301), under the 3000 human-merge threshold",
"files_changed": [
".changeset/26836-access-create-taken-id.md",
"ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx",
"ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessCreateTaken-26836.test.tsx",
"ui/packages/i18n/src/namespaces/studio/en.ts",
"ui/packages/i18n/src/namespaces/studio/zh.ts"
],
"deviations": [
"changeset carries '@object-ui/i18n': patch beside '@object-ui/app-shell': patch (the order named app-shell only); the new studio-namespace rows ship from @object-ui/i18n",
"Discard reuses three existing rows (engine.edit.discardDraft, engine.edit.draftDiscarded) and adds two confirm sentences of its own: the shared engine.edit.discardDraftConfirmBody says the published version stays, which is false for a never-published set",
"narrowed app-shell run (66 files), not the full package; CI Test UI runs the rest"
],
"open_questions": [
{
"question": "Should the metadata door's draft-mode first-write pin (If-None-Match: * with ?mode=draft) also refuse when a PUBLISHED row exists? Measured: no defect today. SaveMetaItemRequestSchema.parentVersion declares a present null asserts 'no current row of this lifecycle', and the metadata-protocol changelog says the pin 'with ?mode=draft for a draft ... saves only where no row of that lifecycle exists'. The door does what the spec declares.",
"options": [
"A — keep the door as declared; each create surface checks published rows client-side (this PR for Access; Interfaces already does). Race window: a set published between the read and the save is not refused.",
"B — a spec + server change making a draft-mode create pin judge every lifecycle (Clause-② semantic, domain:server + spec contract review), closing the race for every client."
],
"recommendation": "A, because the lifecycle-scoped pin is a declared, legitimate meaning ('start a draft only if nobody has one') and the race needs a publish inside a sub-second window; B changes a published contract for every caller with no measured producer hitting the race."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed — Data and Automations creates (doCreateObject / doCreateFlow) carry the same draft-only first-write pin; doCreateObject checks only its own rail, doCreateFlow nothing. Not reproduced here (no measured overwrite of a published object or flow), so recorded only. Dedupe words: studio create published id blank draft, first-write pin lifecycle, doCreateFlow published flow overwrite"
]
}
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
priority:p1·domain:ui·area:access·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T18:56Z⛔ Not a claim, ⛔ not a dispatch.
domain:serverchild. Re-check the landing site on currentmain.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1
Session:
session_01QsR2jTfvjNTaR29ehLk26TAccount:
zhuangjianguoBranch:
claude/project-thread-m5noqq(platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)Worktree:
objectstack-issue-26836Domain:
domain:uiSeat:
domain:ui#4File surface:
ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx(AccessPillar only), studio namespaceen.ts/zh.ts, tests beside, one root changeset; ⛔packages/spec/**, server packages (a door half is reported as adomain:serverchild), any new key (stop on breach; explain in the report)Container & model:
M,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z),effort: highClause-②: no
Metadata-protocol: none
Responsibility: Studio's Access create path (ui app-shell, this lane) writes a blank draft over a published permission set, because its first-write guard covers the draft row only · the platform path that may cover it: the metadata door's
If-None-Match: *, which the thread measures (a server child if it should refuse a published row) · reached by every "+ New" permission set whose name derives to a live set's idThread-read: 6450910097
Premise: holds (
AccessPillar.doCreatesaves withmode: 'draft',ifNoneMatch: '*'and no published-row check, StudioDesignSurface.tsx ~8544-8558, on origin/mainc4a4aefcat 2026-10-27T20:19Z)Serial constraints cleared: open PR #26839 (#26705) touches AccessPillar's held-edit hook (~8473), a different region; PR #26824 touches the slot host only (scan of the 80 most recently updated open PRs 2026-10-27T20:20Z)
Dispatched at 2026-10-27T20:22Z to the card thread on that branch; the order lives in the thread's opening message.
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26836, "status": "done", "branch": "claude/project-thread-m5noqq", "pr": "https://github.com/objectstack-ai/objectstack/pull/26916", "session": "session_01QsR2jRu5CpdGJFtUFzPvUy", "premise_still_valid": true, "summary": "AccessPillar.doCreate now reads the published permission sets and the draft headers across every package (client.list('permission') + client.listDrafts({ type: 'permission' }), the Interfaces create's unscoped pair) before writing; a taken identifier is refused in the dialog with a sentence naming the set, plus an 'Open LABEL' path when the set is on this package's rail. A fresh identifier saves as before (mode draft, packageId, ifNoneMatch '*', surface studio). An open set with a pending draft gets 'Discard draft' in its header: confirm, then client.reset('permission', NAME, { state: 'draft', packageId }) (the metadata editor's discard door, existing API); a published set re-reads its live body (matrix publishNonce + local nonce), a never-published set leaves the rail. Six new studio-namespace rows (en + zh). Door measured: no server child (see open_questions).", "tests": "head 4dd29301. New StudioDesignSurface.accessCreateTaken-26836.test.tsx on the real AccessPillar + PermissionMatrixEditPage: Test Files 1 passed, Tests 9 passed (9), under os-verify-lock. Ablation via scripts/ablation-replace.mjs (anchor 'if (publishedRow || draftRow) {' -> 'if (false && (publishedRow || draftRow)) {', x1 -> x0): Tests 3 failed | 6 passed (the three refusal cases); restored, blob == HEAD b2086038, git diff HEAD empty; tests import the pillar from source, no build in between. app-shell + i18n type-check: VERDICT command-exit 0. Narrowed suites (66 app-shell files naming AccessPillar / studio namespace / engine.studio.access + all of packages/i18n/): Test Files 155 passed (155), Tests 2159 passed | 13 skipped. check:i18n-designer-parity exit 0; check:new-line-citations 0 new.", "gates": "dispatch-gates --commands derived 34 at 4dd29301; all 34 exit 0 (PR-body gates against the final body via GITHUB_EVENT_PATH, re-run after the Verification section: closing-keyword-parity 0, adr-0087-registration 0, changeset-no-major 0, partof-closing-keyword 0). vitest run scripts/: 178 passed | 2 skipped files, 5065 passed tests. check:type-check-debt: 0 raw tsc errors. dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED. CI check-runs on #26916: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "4 — GitHub MCP issue_read x3 (get, get_comments x2) on #26836, all reads; claude-code-remote get_session x1. No MCP write.", "api_writes": "2 — relay repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches x2 (pr_create #26916 with assignees [zhuangjianguo], run 40583946894, read-back identical 7927 bytes; this os-dev-report comment). git push is not a REST write.", "line_budget": "5 files, +491 / -6 (dispatch-gates count at 4dd29301), under the 3000 human-merge threshold", "files_changed": [ ".changeset/26836-access-create-taken-id.md", "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx", "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessCreateTaken-26836.test.tsx", "ui/packages/i18n/src/namespaces/studio/en.ts", "ui/packages/i18n/src/namespaces/studio/zh.ts" ], "deviations": [ "changeset carries '@object-ui/i18n': patch beside '@object-ui/app-shell': patch (the order named app-shell only); the new studio-namespace rows ship from @object-ui/i18n", "Discard reuses three existing rows (engine.edit.discardDraft, engine.edit.draftDiscarded) and adds two confirm sentences of its own: the shared engine.edit.discardDraftConfirmBody says the published version stays, which is false for a never-published set", "narrowed app-shell run (66 files), not the full package; CI Test UI runs the rest" ], "open_questions": [ { "question": "Should the metadata door's draft-mode first-write pin (If-None-Match: * with ?mode=draft) also refuse when a PUBLISHED row exists? Measured: no defect today. SaveMetaItemRequestSchema.parentVersion declares a present null asserts 'no current row of this lifecycle', and the metadata-protocol changelog says the pin 'with ?mode=draft for a draft ... saves only where no row of that lifecycle exists'. The door does what the spec declares.", "options": [ "A — keep the door as declared; each create surface checks published rows client-side (this PR for Access; Interfaces already does). Race window: a set published between the read and the save is not refused.", "B — a spec + server change making a draft-mode create pin judge every lifecycle (Clause-② semantic, domain:server + spec contract review), closing the race for every client." ], "recommendation": "A, because the lifecycle-scoped pin is a declared, legitimate meaning ('start a draft only if nobody has one') and the race needs a publish inside a sub-second window; B changes a published contract for every caller with no measured producer hitting the race." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed — Data and Automations creates (doCreateObject / doCreateFlow) carry the same draft-only first-write pin; doCreateObject checks only its own rail, doCreateFlow nothing. Not reproduced here (no measured overwrite of a published object or flow), so recorded only. Dedupe words: studio create published id blank draft, first-write pin lifecycle, doCreateFlow published flow overwrite" ] }Generated by Claude Code