Skip to content

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

Closed

Description

@objectstack-fleet

Source

Found by the #26836 card thread (PR #26916, out-of-scope finding), checked by the ui seat on origin/main 94e29cd1. 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.

Related: #26836, PR #26916, objectui#11773.

Activity

  1. added
    bugSomething isn't working
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:uiDomain lane — seat card indexed by label:pm:seat
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    pm:queueReady for the PM dispatch loop
    on Oct 27, 2026
  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 → p1 · domain:ui · area:studio · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T21:53Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    priority:p1High: required for production / M2
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    on Oct 27, 2026
  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    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

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

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:uiDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions