Repository navigation
Deleting a draft-only saved view no-ops — deleteView has no draft qualifier, and the mirror of #4139 is NOT mechanical (it would break published+draft pairs) #4479
Description
Activity
Triage:
needs-user-decision— the filer measured that the mechanical mirror of #4139 is wrong (it would silently downgrade "delete view" into "discard draft" on a published+draft pair, a real regression on a pathpersistRuntimeMetadatamakes routine) and lists four concrete open points: whether delete on a pair should remove both homes, ordering/partial-failure semantics, probe-vs-blind, and a cross-transport receipt-shape mismatch. That's API/lifecycle-semantics design, not a bug fix with one obvious shape.Four-axis read:
- Measured business pull: real and current — any published view that's since been edited (any runtime edit stages as a draft per
persistRuntimeMetadata) hits the draft-only-view bug path today; "publish then edit" is described as routine, not a corner case. - Platform long-term coherence: the filer's proposed shape (delete both homes, draft first, widen the receipt so partial outcomes are observable instead of rounded to
true) is coherent withupdateView's existing draft-first precedent and doesn't invent new semantics. - AI-agent error-resistance: the four open points (ordering, partial-failure reporting, probe-vs-blind, cross-transport error-shape agreement) are exactly the kind of details that go wrong silently if an agent picks a shape without a ruling — worth pinning explicitly before dispatch.
- Startup scope discipline: scope is already tight — this is closing a real gap in an existing feature (view delete), not adding new surface.
Recommendation: approve the filer's suggested shape (delete both homes, draft first, widen
{ deleted: boolean }to express a partial outcome) and let the dispatch require red-first coverage for all three rows of the filer's table, including the pair case current code accidentally gets right today.
Generated by Claude Code
- Measured business pull: real and current — any published view that's since been edited (any runtime edit stages as a draft per
CLAIM + PM ruling — session_017Qqyix2QcnpUC9XeYVDzx3, branch
claude/issue-4479-delete-view-both-homes. The triage seat's four-axis read and the filer's suggested shape agree; ruling issued under delegated decision authority, maintainer veto window open — record objections here. The four decision points, settled:- Pair semantics: delete BOTH homes. "Remove this view" is satisfied only when no home is left serving it. Draft-first mirroring would silently downgrade Delete into Discard-draft on the routine publish-then-edit pair; single-call published-delete leaves the draft resurrecting the tab. Both, always.
- Ordering & partial failure: draft first, receipt widened additively. Draft first, because a failure between the two calls then leaves the PUBLISHED overlay intact and the operation cleanly retryable — the reverse order strands a draft-only view, which is this card's original bug shape. Widen the receipt with ADDITIVE optional fields (e.g. per-home outcomes alongside the existing
deleted),deletedtrue only when no home remains; a partial outcome is observable, never rounded to true. MeasureupdateView's error-propagation convention and match it for the failure path. - Probe or blind: pin first, then choose. The server's no-draft and no-published answers get pinned by test before the implementation choice (the card is right that 200
reset:falsevs 404 is worth a test, not an assumption). Blind-two-calls is acceptable ONLY if the pinned no-row answers are benign; otherwise probe-first like updateView. Either way the three-row table is red-first coverage, including the pair case today's code accidentally gets right. - Transport: one boundary, one error shape. Prefer routing both halves through the transport that expresses state qualifiers (the metadataClient, as updateView's draft half does), measured for whether it can serve the published delete too; if cross-transport is genuinely unavoidable, normalize error shapes at the deleteView boundary so callers see one contract. Say which way measurement pointed.
Changeset: '@object-ui/data-objectstack' MINOR (entry-reachable receipt widening + published behavior move — the #4495/#4271 precedent line). Consumers: additive receipt should break none; measure deleteView's call sites and pin each at its read. Mutual exclusion: data-objectstack is free (#4271 landed). ⛔ If any consumer edit lands in #4527-phase-2's write set (ResourceEditPage, StudioDesignSurface, panels, default inspectors, ConditionBuilder), STOP and report. Disjoint from #4553 (gantt), #4533 (CelPredicateField), #4546 (plugin-designer).
Generated by Claude Code
- added a commit that references this issue
on Aug 13, 2026 - added a commit that references this issue
on Aug 17, 2026
Split out of #4463 per the ruling on that card (#4463 comment 5270772373): the secondary defect was to be fixed in the same PR only if teaching
deleteViewthe draft qualifier is the mechanical mirror of whatupdateViewgained in #4139. It is measured below as not mechanical, so it becomes this card. The primary fix (the unboundupdateViewcalls) shipped separately.Symptom (from #4463's QA run)
Deleting a view that exists only as a draft issues
DELETE /api/v1/meta/view/against the published overlay and returnsThe draft survives and the tab is still present after reload. Delete on a published view works correctly (
reset:true, tab gone).Where it is
packages/data-objectstack/src/index.ts—deleteViewis unqualified:updateView, by contrast, probes the draft first (metaClient.get('view', name, { state: 'draft' })) and writes back to whichever home the read resolved.Why the mirror is NOT mechanical — measured
The draft-discard primitive exists and is reachable, so a naive mirror is easy to write:
MetadataClient.reset(type, name, { state: 'draft' })issuesDELETE /meta/:type/:name?state=draft(packages/data-objectstack/src/metadata-client.ts).MetadataDeleteOptions.stateis documented there as: "'draft'discards the pending draft (keeps the published overlay intact)."deleteMetaItemin frameworkpackages/metadata-protocol/src/protocol.tsprobesrepo.get(ref, { state: targetState })and targets only that state.The problem is what draft-first means for delete, in the case where a view has both homes:
reset:false, draft survivesreset:true, tab goneThe third row is not a corner case.
persistRuntimeMetadata(packages/app-shell/src/views/runtime-metadata-persistence.ts) stages every runtime edit as a draft —metadataClient.save(type, name, body, { mode: 'draft' })— so "publish a view, then edit it" routinely produces a published+draft pair. A draft-first delete on that pair silently turns Delete view into Discard draft.And Discard draft is a distinct operation that already exists, in the same file:
documented as "Discard the pending draft of a runtime artifact (Studio's 'Discard draft'). The published overlay is untouched." So the mechanical mirror would make
deleteViewcollide with an existing, deliberately different semantic.The asymmetry has a clean statement: for update, draft-first is right in all three cases, because a draft shadows the published row and Publish would otherwise overwrite the edit with the pre-edit body (the reasoning written into
updateView's docblock as point 2). For delete, "remove this view" is only satisfied when no home is left serving it — which is one call in two of the three cases and two calls in the third.What has to be decided (why this needs its own card)
deleteViewon a published+draft pair remove both homes? That is the only reading under which the tab reliably disappears, but it is two calls, not one.{ deleted: boolean }report when one succeeds and the other fails? The current return type cannot express "draft gone, overlay left".reset(..., { state: 'draft' })throws on any non-OK response, so a blind two-call implementation needs the no-draft answer pinned first (the server's no-row branch returns a 200reset:falserather than a 404, but that is worth a test rather than an assumption).deleteViewcurrently goes throughthis.client.meta.deleteItem, whileupdateView's draft half usesthis.metadataClient(). The mirror crosses transports, so the two halves need to agree on error shapes.Suggested shape (not a ruling)
Delete both homes, draft first, and widen the receipt so a partial outcome is observable rather than rounded to
true. Red-first coverage for all three rows of the table above, including the pair case that today's code accidentally gets right.Provenance
Measured while implementing #4463 (branch
claude/issue-4463-setdefault-unbound). No code for this half was written; the primary card's PR is scoped to the unbound-call fix.Generated by Claude Code