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: the Automations and App creates can write a blank draft over a published flow or app of the same identifier (first-write pin is draft-only) #26917
Found by the #26836 card thread (PR #26916, out-of-scope finding), checked by the ui seat on origin/main94e29cd1. Not reproduced end to end yet: the mechanism is #26836's, read from the code.
Problem
Studio's creates save the blank skeleton with { mode: 'draft', ifNoneMatch: '*' }. The door's first-write pin is lifecycle-scoped (SaveMetaItemRequestSchema.parentVersion: null means "no current row of this lifecycle"), so it refuses only where a DRAFT row exists. Over a PUBLISHED row of the same identifier it writes the skeleton as that item's pending draft, and a package publish then replaces the live item with the blank one. #26836 measured this for permission sets; PR #26916 fixes Access by reading published and draft rows before the save, as the Interfaces create already does (StudioDesignSurface.tsx ~4225).
The same pin, with no published-row read, remains in ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:
doCreateFlow (~7452, save ~7481): no taken-identifier check at all.
doCreateApp (~1680, save ~1694): no taken-identifier check.
doCreateObject (~5976, save ~5992): checks only the objects on its own rail (objects.some), not published or draft rows elsewhere. The namespace prefix makes a cross-package clash less likely, not impossible.
Target
Measure each create first: name a published flow, app and object, create one with the same identifier, and check whether a draft lands over it. For each create that does, read the published rows and the drafts of that type before the save (the Interfaces/Access shape), refuse a taken identifier in the dialog naming the holder, and keep the first-write pin for the race. Report any create that is already safe.
File surface
StudioDesignSurface.tsx (the three create callbacks and their dialogs), studio namespace en.ts/zh.ts for the refusal sentences, tests beside, one root changeset. No packages/spec/**, no server change: the pin's lifecycle scope is the declared contract (the #26836 thread's open question, answered A by the ui seat).
Acceptance
For each affected create, an identifier held by a published item of that type is refused before anything is written, and the published item's body is unchanged. A fresh identifier creates as before.
Claim: PM loop round 1
Session: session_01QsR2jTfvjNTaR29ehLk26T
Account: zhuangjianguo
Branch: claude/project-thread-m23vgi (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-26917
Domain: domain:ui
Seat: domain:ui#4
File surface: ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the flow, app and object create callbacks and their dialogs, plus a shared helper), studio namespace en.ts/zh.ts, tests beside, one root changeset; ⛔ packages/spec/**, server packages, any new metadata 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 create dialogs (ui app-shell, this lane) can save a blank draft over a published flow, app or object of the same id · the platform path that may cover it: the door's first-write pin, which is lifecycle-scoped by the spec's declaration (answered A in PR #26916's ACCEPT) · reached by every Studio author who reuses an identifier
Thread-read: 6454560753
Premise: holds (doCreateFlow and doCreateApp save with { mode: 'draft', ifNoneMatch: '*' } and no taken-id read at StudioDesignSurface.tsx ~7481 and ~1694, and doCreateObject checks only its own rail at ~5982, on origin/main 7a2818c5 at 2026-10-27T22:15Z)
Serial constraints cleared: PR #26916 (the Access create) merged at 22:01Z; no other in-flight card or open PR of this seat edits StudioDesignSurface.tsx
Dispatched at 2026-10-27T22:20Z to the card thread on that branch; the order lives in the thread's opening message.
{
"issue": 26917,
"status": "done",
"branch": "claude/project-thread-m23vgi",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26987",
"head": "efc675b9",
"session": "session_01QsR2jUEVZh2LeEfPDe25SW",
"premise_still_valid": true,
"measured": "Door probe (scratch, uncommitted) on ObjectStackProtocolImplementation.saveMetaItem with the served-content-hash engine double, both packages installed: publish, then mode 'draft' + parentVersion null from com.acme.app. flow, app, object (and permission as control) each WRITE the draft over the published row, same package and other package alike. No create was already safe; doCreateObject refused only its own rail.",
"summary": "One shared findTakenIdentifier(client, type, name, locale) (unscoped client.list(type) + client.listDrafts({ type })) and CreateTakenNotice in StudioDesignSurface.tsx. doCreateFlow, doCreateApp and doCreateObject refuse a taken identifier before any write, naming the holder; Open for a flow/object on the pillar's rail; app has no Open (Create app shows only for a package with no app). AccessPillar now uses the same helper and notice, behaviour and test ids unchanged. Fresh ids save as before with mode draft, packageId, ifNoneMatch '*', surface studio. Merged main b1b153a3 (#26927 asks on New before the dialog), so Open asks nothing more.",
"tests": "head efc675b9. New StudioDesignSurface.createTaken-26917.test.tsx (8) + Access 26836 (9) + #26927 held*Create (8): 5 files, 25 passed. Ablation via ablation-replace (findTakenIdentifier returns null): 7 failed | 10 passed (the 7 helper-routed refusals), restored blob == HEAD. app-shell + i18n type-check exit 0. eslint 0 errors (14 warnings, same as main). Narrowed suites (178 app-shell files + packages/i18n/): 267 files, 2780 passed | 13 skipped. i18n-designer-parity exit 0; new-line-citations 0.",
"gates": "dispatch-gates --commands derived 34 (same list before and after merging main); all 34 exit 0 at efc675b9, PR-body gates against the final body (closing-keyword-parity, adr-0087-registration, changeset-no-major via GITHUB_EVENT_PATH; partof-closing-keyword via PR_BODY: 0). vitest run scripts/: 178 passed | 2 skipped files. check:type-check-debt: 0 raw tsc errors. --ran: 34 run, 0 NOT-MEASURED. CI check-runs on #26987: not yet started at report time.",
"dogfood_battery": "n/a: no `!` in the title and no (narrowing) arm",
"mcp_calls": "5: GitHub MCP issue_read x4 on #26917 (get x1, get_comments x3), 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 #26987 with assignees [zhuangjianguo], run 40607212638, read-back identical 10258 bytes; this os-dev-report comment). git push x2 is not a REST write. Two unauthenticated public REST reads (the claim comment 6455050547; #26836's comments for the report shape).",
"line_budget": "5 files, +719 / -165 (dispatch-gates count at efc675b9), under the 3000 human-merge threshold; ~140 of the + and - lines are re-indentation of two dialog `extra` blocks wrapped in a fragment",
"files_changed": [
".changeset/26917-studio-create-taken-id.md",
"ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx",
"ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.createTaken-26917.test.tsx",
"ui/packages/i18n/src/namespaces/studio/en.ts",
"ui/packages/i18n/src/namespaces/studio/zh.ts"
],
"deviations": [
"AccessPillar edited (inside the claimed file): its inline check and refusal markup replaced by the shared helper and notice, per the order's 'one helper over the type, not three copies'; its sentences and test ids unchanged, its 9 tests green",
"i18n: engine.studio.access.openExisting renamed to the shared engine.studio.create.openExisting; engine.studio.data.idExists removed (its only reader is replaced by the new refusal)",
"changeset carries '@object-ui/i18n': patch beside '@object-ui/app-shell': patch, as #26836 did",
"branch merged origin/main b1b153a3 mid-card (clean); #26927 had moved the held-edit question to the New buttons, so the refusal's Open dropped its own confirm",
"narrowed app-shell run (178 files), not the full package; CI Test UI runs the rest",
"the first ablation run (before the first commit) restored the file from HEAD and wiped the uncommitted edits; they were re-applied from the same edit script, blob-identical (8c9301f1) before committing"
],
"open_questions": [],
"out_of_scope_findings": []
}
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: p2 → p1 ·
domain:ui·area:studio· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T21:53Z⛔ Not a claim.
findingremoved.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1
Session:
session_01QsR2jTfvjNTaR29ehLk26TAccount:
zhuangjianguoBranch:
claude/project-thread-m23vgi(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-26917Domain:
domain:uiSeat:
domain:ui#4File surface:
ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx(the flow, app and object create callbacks and their dialogs, plus a shared helper), studio namespaceen.ts/zh.ts, tests beside, one root changeset; ⛔packages/spec/**, server packages, any new metadata 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 create dialogs (ui app-shell, this lane) can save a blank draft over a published flow, app or object of the same id · the platform path that may cover it: the door's first-write pin, which is lifecycle-scoped by the spec's declaration (answered A in PR #26916's ACCEPT) · reached by every Studio author who reuses an identifier
Thread-read: 6454560753
Premise: holds (
doCreateFlowanddoCreateAppsave with{ mode: 'draft', ifNoneMatch: '*' }and no taken-id read at StudioDesignSurface.tsx ~7481 and ~1694, anddoCreateObjectchecks only its own rail at ~5982, on origin/main7a2818c5at 2026-10-27T22:15Z)Serial constraints cleared: PR #26916 (the Access create) merged at 22:01Z; no other in-flight card or open PR of this seat edits StudioDesignSurface.tsx
Dispatched at 2026-10-27T22:20Z to the card thread on that branch; the order lives in the thread's opening message.
Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
os-dev-report
{ "issue": 26917, "status": "done", "branch": "claude/project-thread-m23vgi", "pr": "https://github.com/objectstack-ai/objectstack/pull/26987", "head": "efc675b9", "session": "session_01QsR2jUEVZh2LeEfPDe25SW", "premise_still_valid": true, "measured": "Door probe (scratch, uncommitted) on ObjectStackProtocolImplementation.saveMetaItem with the served-content-hash engine double, both packages installed: publish, then mode 'draft' + parentVersion null from com.acme.app. flow, app, object (and permission as control) each WRITE the draft over the published row, same package and other package alike. No create was already safe; doCreateObject refused only its own rail.", "summary": "One shared findTakenIdentifier(client, type, name, locale) (unscoped client.list(type) + client.listDrafts({ type })) and CreateTakenNotice in StudioDesignSurface.tsx. doCreateFlow, doCreateApp and doCreateObject refuse a taken identifier before any write, naming the holder; Open for a flow/object on the pillar's rail; app has no Open (Create app shows only for a package with no app). AccessPillar now uses the same helper and notice, behaviour and test ids unchanged. Fresh ids save as before with mode draft, packageId, ifNoneMatch '*', surface studio. Merged main b1b153a3 (#26927 asks on New before the dialog), so Open asks nothing more.", "tests": "head efc675b9. New StudioDesignSurface.createTaken-26917.test.tsx (8) + Access 26836 (9) + #26927 held*Create (8): 5 files, 25 passed. Ablation via ablation-replace (findTakenIdentifier returns null): 7 failed | 10 passed (the 7 helper-routed refusals), restored blob == HEAD. app-shell + i18n type-check exit 0. eslint 0 errors (14 warnings, same as main). Narrowed suites (178 app-shell files + packages/i18n/): 267 files, 2780 passed | 13 skipped. i18n-designer-parity exit 0; new-line-citations 0.", "gates": "dispatch-gates --commands derived 34 (same list before and after merging main); all 34 exit 0 at efc675b9, PR-body gates against the final body (closing-keyword-parity, adr-0087-registration, changeset-no-major via GITHUB_EVENT_PATH; partof-closing-keyword via PR_BODY: 0). vitest run scripts/: 178 passed | 2 skipped files. check:type-check-debt: 0 raw tsc errors. --ran: 34 run, 0 NOT-MEASURED. CI check-runs on #26987: not yet started at report time.", "dogfood_battery": "n/a: no `!` in the title and no (narrowing) arm", "mcp_calls": "5: GitHub MCP issue_read x4 on #26917 (get x1, get_comments x3), 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 #26987 with assignees [zhuangjianguo], run 40607212638, read-back identical 10258 bytes; this os-dev-report comment). git push x2 is not a REST write. Two unauthenticated public REST reads (the claim comment 6455050547; #26836's comments for the report shape).", "line_budget": "5 files, +719 / -165 (dispatch-gates count at efc675b9), under the 3000 human-merge threshold; ~140 of the + and - lines are re-indentation of two dialog `extra` blocks wrapped in a fragment", "files_changed": [ ".changeset/26917-studio-create-taken-id.md", "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx", "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.createTaken-26917.test.tsx", "ui/packages/i18n/src/namespaces/studio/en.ts", "ui/packages/i18n/src/namespaces/studio/zh.ts" ], "deviations": [ "AccessPillar edited (inside the claimed file): its inline check and refusal markup replaced by the shared helper and notice, per the order's 'one helper over the type, not three copies'; its sentences and test ids unchanged, its 9 tests green", "i18n: engine.studio.access.openExisting renamed to the shared engine.studio.create.openExisting; engine.studio.data.idExists removed (its only reader is replaced by the new refusal)", "changeset carries '@object-ui/i18n': patch beside '@object-ui/app-shell': patch, as #26836 did", "branch merged origin/main b1b153a3 mid-card (clean); #26927 had moved the held-edit question to the New buttons, so the refusal's Open dropped its own confirm", "narrowed app-shell run (178 files), not the full package; CI Test UI runs the rest", "the first ablation run (before the first commit) restored the file from HEAD and wiped the uncommitted edits; they were re-applied from the same edit script, blob-identical (8c9301f1) before committing" ], "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code