Skip to content

fix(app-shell): Studio's Automations, App and Data creates refuse an identifier a published or draft item already holds - #26987

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-m23vgi
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-m23vgi

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #26917

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 的「新建自动化」「创建应用」「新建对象」在写入前,先查同类型已发布和草稿中的同名项(跨所有软件包);标识已被占用就拒绝、不写入,提示里点名占用者;占用者在本支柱左栏里(本软件包的流程或对象)时给出「打开它」。三处与「访问」页共用一个检查函数。
  • 为什么改:首写保护(If-None-Match: * + 草稿模式)只拦已有草稿,不拦已发布的同名项。实测(见下)三种类型都会在已发布项上叠一份空白草稿,发布软件包就会用空白项替换线上的流程、应用或对象。
  • 风险与回滚:低。只改 StudioDesignSurface.tsx 的三个新建回调与对话框,沿用现有接口(列表、草稿列表);服务端、spec 不动。回滚即还原本 PR。
  • 先看:findTakenIdentifier 与 CreateTakenNotice(文件上部),doCreateFlow / doCreateApp / doCreateObject 开头的占用检查;StudioDesignSurface.createTaken-26917.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.createTaken-26917.test.tsx,预期 8 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

  • One shared check. findTakenIdentifier(client, type, name, locale) reads the published items and the draft headers of a type across every package (client.list(type), client.listDrafts({ type }), the unscoped pair the Interfaces and Access creates read) and names the item holding the identifier, or null. CreateTakenNotice renders the refusal in the create dialog: a sentence naming the holder and, when the holder is on the pillar's own rail, an "Open “LABEL”" button. The Access create (fix(app-shell): Studio Access refuses a new permission set over a taken identifier and can discard a set's draft #26916) now uses both instead of its inline copy; its behaviour, sentences and test ids are unchanged.
  • New automation (doCreateFlow): an identifier a flow holds is refused before anything is written. A flow on this package's rail gets "Open", which closes the dialog and opens it (the New button already asked about a held step, fix(app-shell): creating an item or opening one from Pending changes asks before it drops a held Studio edit #26927, so Open asks nothing more); a flow another package holds gets no open path.
  • New object (doCreateObject): the rail check it had (same package only) becomes the "here" arm of the same refusal, with "Open"; an object another package holds (the namespace-prefixed identifier) is now refused too.
  • Create app (doCreateApp): an identifier an app holds is refused. Create app is offered only to a package with no app, so the holder is always another package's and there is no "Open".
  • A fresh identifier is created exactly as before: the draft save with mode: 'draft', packageId, ifNoneMatch: '*', surface: 'studio'. The pin still refuses a draft that lands between the check and the save.
  • Strings (@object-ui/i18n, studio namespace, en and zh): five refusal rows (engine.studio.auto.createTaken, …auto.createTakenElsewhere, …app.createTaken, …data.createTaken, …data.createTakenElsewhere); engine.studio.access.openExisting becomes the shared engine.studio.create.openExisting; engine.studio.data.idExists, which the new refusal replaces, is removed.
  • One root changeset: @object-ui/app-shell patch, @object-ui/i18n patch. No packages/spec/**, no server change, no metadata key.

Why

The card's order was to measure each create first. Measured at the metadata door (ObjectStackProtocolImplementation.saveMetaItem, the in-memory engine double of protocol.served-content-hash.test.ts, both packages installed in the registry): publish an item, then save a draft of the same name with parentVersion: null (the REST door's If-None-Match: *) and mode: 'draft', as each create does.

type published in draft saved from result
flow same package com.acme.app written: active row "Live" + draft row "Blank"
flow other package com.acme.app written: active row (other) + draft row (acme)
app same / other package com.acme.app written over it, both cases
object same / other package com.acme.app written over it, both cases
permission (control, #26836) same / other package com.acme.app written over it, both cases

So no create was already safe at the door. The Data create was safe only for an object on its own rail (same package), which it already refused; the other two checked nothing. The probe was a scratch test, not committed.

Tests

StudioDesignSurface.createTaken-26917.test.tsx, eight cases on the real AutomationsPillar, DataPillar and StudioDesignSurface (for Create app), over a client double that keeps published and draft rows per package, writes a draft over a published row the way the door does, and refuses the pin over a draft row:

  • flow on this package's rail: refused, save never called, no draft row, the published body unchanged, the sentence names it, and "Open" opens it (its draft read follows);
  • flow another package holds: refused, nothing written, body unchanged, no "Open";
  • flow, fresh identifier: saved with the four options above;
  • object another package holds (acme_ticket): refused, nothing written, body unchanged, no "Open";
  • object on this package's rail: refused, nothing written, "Open" opens it;
  • object, fresh identifier: saved with the four options;
  • app another package holds: refused, nothing written, body unchanged, no "Open";
  • app, fresh identifier: saved with the four options.

Acceptance notes

Verification

All at head efc675b9 (this branch merged origin/main b1b153a3, which brought #26927's held-edit question on New), in a worktree, every build and test under scripts/pm/os-verify-lock.sh.

New tests, ablation, type-check, lint and nearby suites
  • pnpm --dir ui exec vitest run on the new file, the Access file (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) and fix(app-shell): creating an item or opening one from Pending changes asks before it drops a held Studio edit #26927's three held*Create-26845 files: Test Files 5 passed (5), Tests 25 passed (25).
  • Ablation (node scripts/ablation-replace.mjs, anchor if (!publishedRow && !draftRow) return null; replaced by if (true || (!publishedRow && !draftRow)) return null;, anchor x1 to x0) over the new file and the Access file: Tests 7 failed | 10 passed (17), exactly the seven refusals that go through findTakenIdentifier (three Access, two flow, object-elsewhere, app); the object-on-rail case stays green because it is refused by the rail arm before the read. Restored: blob equal to HEAD, git diff HEAD empty.
  • pnpm --filter @object-ui/app-shell run type-check && pnpm --filter @object-ui/i18n run type-check: VERDICT command-exit 0 (app-shell's covers the test file).
  • eslint on the two app-shell files and the two catalogues: 0 errors; StudioDesignSurface.tsx carries the same 14 warnings as on origin/main.
  • Narrowed suites: every app-shell test file naming AutomationsPillar, DataPillar, AccessPillar, StudioDesignSurface or an engine.studio.(auto|data|app|access|create). key (178 files), plus all of packages/i18n/: Test Files 267 passed (267), Tests 2780 passed | 13 skipped (2793). CI's Test UI runs the rest of app-shell.
  • pnpm run check:i18n-designer-parity (from ui/): "Every en row has a zh row, and every shared row carries the same placeholders." pnpm check:new-line-citations: 0 new citation(s).
Derived dispatch gates (34/34)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands (the same list before and after the merge of main); every one exited 0 at efc675b9, the PR-body gates (check-closing-keyword-parity, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main) against this body through GITHUB_EVENT_PATH, and check-partof-closing-keyword with PR_BODY: exit 0.
  • pnpm --dir ui exec vitest run scripts/: Test Files 178 passed | 2 skipped (180), Tests 5065 passed | 2 skipped (5067).
  • pnpm check:type-check-debt: 0 raw tsc error(s) total, none above its recorded number.
  • dispatch-gates --ran: 34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED.

Dogfood battery: not run, no ! in the title and no (narrowing) arm.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the touched files: 0 errors; the full lint runs in CI)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (the 维护者速读 section is the maintainer's Chinese summary)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jUEVZh2LeEfPDe25SW

…identifier a published or draft item already holds

The draft-mode first-write pin refuses only a DRAFT row, so "New
automation", "Create app" and "New object" wrote their blank skeleton
as the pending draft of a published flow, app or object of the same
identifier, and a package publish would have replaced the live item.

Each create now reads the published items and drafts of its type
across every package first (one shared `findTakenIdentifier`, which
the Access create now uses too) and refuses a taken identifier in the
dialog, naming the holder and offering to open it when it is on the
pillar's rail. A fresh identifier saves as before, under the pin.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUEVZh2LeEfPDe25SW
Since the New button asks before the dialog opens whether to drop a held
edit, opening the flow or object that holds the identifier needs no
second question.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUEVZh2LeEfPDe25SW
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#4 · session_01QsR2jTfvjNTaR29ehLk26T · 2026-10-28T00:42Z · head 9afbfa0ccfb317f2c86ad6e5a3c746271337b0f8

  1. Shape: draft, base main, Fixes #26917. Clause-②: no and Metadata-protocol: none are in the body and the changeset, and they match the claim (6455050547). Five files, all inside the claimed surface: StudioDesignSurface.tsx (the flow, app and object creates, their dialogs, and a shared helper), the new StudioDesignSurface.createTaken-26917.test.tsx, the studio en.ts/zh.ts, and one root changeset (@object-ui/app-shell and @object-ui/i18n patch). There is no packages/spec/**, no server package and no metadata key.
  2. Premise, measured: the seat read the diff.
    • findTakenIdentifier(client, type, name, locale) reads client.list(type) and client.listDrafts({ type }) across every package. It returns the holder's name and label, or null.
    • doCreateFlow, doCreateApp and doCreateObject each call it before the save and return early, so nothing is written. The object create keeps its rail fast path, and that path is now the "here" arm of the same refusal.
    • CreateTakenNotice renders the sentence. It offers "Open" only for a holder on the pillar's own rail. The Access create (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) now uses both pieces, with the same sentences and test ids.
    • A fresh identifier saves as before (mode: 'draft', packageId, ifNoneMatch: '*', surface: 'studio'), and the pin still refuses a draft that lands after the check.
    • The thread's door probe (report 6457061846) showed that all three creates wrote a draft over a published row before this change.
    • The new file has 8 cases on the real pillars, and the ablation reds 7.
    • The studio keys removed, engine.studio.data.idExists and engine.studio.access.openExisting, have 0 remaining readers on the head.
  3. Gates, pinned to the head:
    • check-runs?filter=latest: 43 success, 10 skipped, 0 failing or pending.
    • check-governed-merges --pr 26987: NOT governed (0 of 5 paths), +719/−165.
    • mergeable_state: clean.
    • origin/main 31a608eb has no findTakenIdentifier, so nothing here has already landed.
  4. Accepted deviations:
  5. Out of scope:
  6. Prose face, checked against the diff:
    • The changeset's Before/Now/Strings bullets match the three call sites and the five new rows in each locale.
    • Create app's one sentence names no package, because Create app is offered only to a package with no app.

Landing: no contract review is owed. There is no published schema, no governed text and no exported type, and Clause-② is no. The seat marks the PR ready and arms auto-merge. On MERGED, Fixes #26917 closes the card.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 00:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 00:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit d7df639 Oct 28, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-m23vgi branch October 28, 2026 01:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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)

2 participants