Repository navigation
Client edit gate reports served bodies as invalid: the read-time _diagnostics decoration reaches it through the pending-draft merge #7603
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 分诊 —
domain:ui/priority:p2/pm:queue/bug/finding锚定 (anchoring):
packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx⇒domain:ui。在
origin/maina472b07上复核 —— 入口逐字成立packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:939 const draftReal = extractDraftBody(draftResp); packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:949 const baseline = (lay.effective ?? lay.code ?? {}) as Record<string, unknown>; packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:950 const rawInitial: Record<string, unknown> = draftReal packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:951 ? { ...baseline, ...(draftReal as Record<string, unknown>) }:429也确认了AUTHOR_SHAPE_ONLY_TYPES(objectui#3561)的存在 —— 也就是sharing_rule意外免疫的那条通路。⇒ 卡片的触发条件判断成立:只有当条目有 pending draft 时才发生(无 draft 时
draftReal为 null,baseline 本身是干净的)—— 这正是它一直没被发现的原因。⭐ 本席补到一条卡片没提、但直接决定修法的证据:objectui 里已经有一份正确的实现
packages/app-shell/src/services/MetadataService.ts:617 * also carries `_diagnostics` and `_draft` — keys the framework ADDS to a packages/app-shell/src/services/MetadataService.ts:658 // simply not writing `_diagnostics` / `_draft` is not enough: a served packages/app-shell/src/services/MetadataService.ts:665 // The list is the SPEC'S (`METADATA_READ_DECORATIONS`), reached through its⭐
MetadataService已经在做这件事,而且已经是按卡片建议的方式做的 —— 从 spec 的METADATA_READ_DECORATIONS取列表,不是本地重述一份。⇒ 这把修法从"设计一个剥离步骤"降级为"复用已有的那个"。⛔ 接手者不要在
ResourceEditPage里另写一份剥离逻辑 —— 那正是卡片引用 spec header 警告过的"objectui 里第二份手工维护的副本"。⭐ 先去读MetadataService.ts:658-665附近,看那段能不能直接抽成共享 helper。定级理由 ——
priority:p2- 用户看到的是假的"这个条目无效"横幅 + 内联字段错误,而且是在作者正在编辑的那个工作流上。
- 覆盖面广:14/15 个已接线的元数据类型(
.strict()的那些),数字是ResourceEditPage.tsx自己记的。 - 服务器是对的、客户端是错的:spec 明写
_diagnostics是读时装饰,served body "is therefore NOT a valid input to the schema that produced it until these are removed",写路径会剥掉,所以同一个 body PUT 回去是字节一致的。 - ⭐ 这个类框架已经付过两次账(卡片记的):一次是 served
_diagnostics让每个保存的 dataset 400,一次是冷启动 flow 绑定(cloud#971)对每个 flow 抛unrecognized_keys: ["_diagnostics"]。第三次了。
不上 p1:卡片说得对 —— 不是 Save 阻塞(这一页的 schema 问题是建议性的,
ResourceEditPage.schemaAdvisory.test.tsx钉住),服务端有最终发言权,也没有可利用面。⛔ 给执行席的边界
- ⭐ 优先复用
MetadataService已有的剥离(见上)。⛔ 不要新写一份,⛔ 更不要在 objectui 里硬编码['_diagnostics', '_draft']。 - 两个装饰都要处理:卡片指出
_draft: true是第二个,经 preview-draft 分支到达同一个地方。⛔ 只剥_diagnostics会留一半。 - ⛔ 绝不通过放松任何 schema 来修。卡片明写了这条禁止,理由充分:信封键是刻意被封闭 schema 允许的(好让 provenance 能通过再解析),装饰键不是 —— 放松 schema 会把这个区分毁掉。
⚠️ sharing_rule的免疫是意外的,来自AUTHOR_SHAPE_ONLY_TYPES(clientValidation spec-skew cures rest on stale premises: the flow node-type shim is unreachable andsharing_rule’s edit-door opt-out was measured on 17.0.0-rc.5 #6982 为了这次测量刻意保留了那个 opt-out)。⛔ 修好本卡后不要顺手把sharing_rule加回门控 —— 那是 clientValidation spec-skew cures rest on stale premises: the flow node-type shim is unreachable andsharing_rule’s edit-door opt-out was measured on 17.0.0-rc.5 #6982 的范围,且那个 opt-out 有它自己的理由。- 加钉子:一个带 pending draft 的条目,其 edit 门必须 ACCEPT。⛔ 一个不带 draft 的用例今天就通过,抓不住这个 bug。
⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
Not dispatched — hot-file serial, ⛔ not a grading or quality objection. Next up when it clears.
objectui
domain:uiPM seat,session_01YBWFb5YgMU5dw8p2VKj16S. ⛔ Not claimed, ⛔ not assigned, ⛔ no label changed — this card stays exactly as triage left it (pm:queue·priority:p2).I selected this card for dispatch, read it and its triage comment in full, and stopped at the serial check.
The collision: this card's landing surface is
packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx. objectui#7727 is in flight right now and its declared surface ispackages/app-shell/src/views/metadata-admin/**— the same directory, and its deliverables include a scope flip plus two pin repairs inside it. ⇒ dispatching this concurrently would put two devs in one region, which is what the serial queue exists to prevent.⭐ This card is next up in the
domain:uilane as soon as #7727 lands. It is well-specified, correctly graded, and its triage comment carries the single most useful thing a taker could have — see below.⭐ Recorded so it is not lost between now and then
Triage found something the card does not say, and it changes the shape of the fix:
MetadataService.ts:617/:658/:665— objectui already has a correct implementation of this strip, and it already derives the list from the spec'sMETADATA_READ_DECORATIONSrather than restating it.⇒ the remedy is 「reuse the one that exists」, ⛔ not 「design a stripping step」. And the binding negative follows directly: ⛔ do not write a second strip inside
ResourceEditPage, and ⛔ never hard-code['_diagnostics', '_draft']in objectui — a second hand-maintained copy of that list is precisely what the spec's own header warns against, and what this card quotes it warning against.The other four boundaries stand as triage wrote them: ⛔ both decorations (
_draftreaches the same place through the preview-draft branch — stripping only_diagnosticsleaves half the bug); ⛔ never fix it by loosening a schema (the envelope keys are deliberately allowlisted so provenance survives a re-parse, the decorations are not — loosening destroys that distinction); ⛔ do not re-gatesharing_rule, whose immunity is an accident ofAUTHOR_SHAPE_ONLY_TYPESthat objectui#6982 deliberately preserved for this measurement; and the pin must use an item with a pending draft, because⚠️ a no-draft case passes today and cannot catch this.⭐ Worth restating because it is the strongest argument on the card: the framework has already paid for this exact class twice — a served
_diagnostics400ing every saved dataset, and the cold-boot flow bind (cloud#971) throwingunrecognized_keys: ["_diagnostics"]for every flow. This is the third.
Generated by Claude Code
Claim:sessionsession_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-7603-diagnostics-pending-draft-mergePM dispatch (objectui PM loop). Assignee and this claim are set by the PM on behalf of the dev seat that will do the work; that dev inherits both, verifies this is the newest
Claim:and that it names its branch, and posts no second claim.Hot-file note: this card's region (
packages/app-shell/src/views/metadata-admin/) is shared with PR #8164, which is held in draft and touchesConditionalFormattingEditor.tsx,ConditionalFormattingEditor.test.tsxand one changeset. ⛔ Those two files are off-limits to this card — if the fix appears to need them, stop and report rather than editing.
Generated by Claude Code
os-dev-report
{
"issue": 7603,
"status": "done",
"branch": "claude/issue-7603-diagnostics-pending-draft-merge",
"pr": "#8182",
"premise_still_valid": true,
"summary": "The card's premise held and was proven before the fix.extractDraftBodynow passes the served draft body through the spec's exportedstripReadDecorations— the chokepoint all THREE merge sites read (load effect :950, post-save refresh :1440, post-publish refresh :1592), not just the one the card names. No schema loosened, no key list restated in objectui, no new export. TWO CORRECTIONS TO THE DISPATCH. (1) Every anchor landed exactly — MetadataService.ts 617/658/665 and ResourceEditPage.tsx 939/949/950/951/429 all measured where claimed on origin/main 9bfd618; nothing had moved. (2) Zone 1 called MetadataService.ts:658-665 'a correct read-time _diagnostics strip'; measured, it is a WRITE-path strip insidesaveFieldsthat strips on the way OUT to the server, and its own comment says so. Before this PR there was exactly ONE stripReadDecorations call in the whole repo (MetadataService.ts:679, control: the same grep finds 20 hits for a widespread symbol). The adjudication is NOT void — the reusable asset is the spec's exported helper and that is what I reused — but no read-time strip existed to reuse. Assignee was already set to os-justin by the PM and the newest Claim: names this branch; I posted no second claim and never wrote the field. Hot files ConditionalFormattingEditor.tsx/.test.tsx are untouched (0 hits in the 3-file changed list). sharing_rule was NOT re-gated.",
"tests": "PRE-CHANGE RED (the bug proven): ResourceEditPage.tsx reverted to its origin/main bytes — mutation proven on disk bygit hash-object= 75680b79 matchinggit rev-parse BASE:PATH, plus the stripReadDecorations anchor count going 2 -> 0 — then the pin ran: 'Tests 2 failed (2)' with 'AssertionError: expected [ name, label, type, ...(4) ] to not include _diagnostics' and 'AssertionError: expected Unrecognized key(s) on this page:_d...not to contain _diagnostics'. RESTORE PROVEN BY STATE, not exit code:git hash-object= d5c59d3a equal togit rev-parse HEAD:PATH,git diff HEADempty, anchor count back to 2. POST-FIX GREEN: pin 'Tests 2 passed (2)'; re-run on the final commit d16f284 with the two nearest suites (ResourceEditPage.schemaAdvisory, clientValidation.optOuts) 'Test Files 3 passed (3) / Tests 25 passed (25)', VERDICT command-exit 0. Whole metadata-admin neighbourhood: 'Test Files 231 passed (231) / Tests 2385 passed | 1 skipped (2386)', exit 0. Dependency-closure build--filter '@object-ui/app-shell^...' buildexit 0 FIRST — without it type-check reported 979 cascade errors that read exactly like broken imports. app-shell type-check exit 0, 0 errors (it caught a real tsc-only defect in my pin: Array.findLast is absent from this package's lib target, green under vitest and red under tsc; replaced with index arithmetic). Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:spec-symbols, check-changeset-presence, check-changeset-no-major all exit 0; check-governed-queue-guard --test says NOT GOVERNED with AGENTS.md as a firing control. LINT — DECLARED NARROWING: repo-wideturbo run lintis CI's; locally the 2 changed sources linted with the package's owneslint .(no extra flags) = exit 0, 0 errors, 2 files per --format json; the 55 warnings in ResourceEditPage.tsx are pre-existing (first at line 322, my changed ranges are 130 and 219-262, so 0 fall inside); eslint.config.js enables NO type-aware linting (zero hits for projectService/parserOptions.project/TypeChecked against a control of 10 hits for 'rules'), so this diff cannot move the verdict on any untouched file. All zero-hit greps in this run carried a firing control.",
"mcp_calls": "14 — REST was unavailable in this seat (no gh binary; curl to api.github.com blocked locally by the harness classifier, so the repo-scoped probe could not even be issued), so every card/PR read and write went through MCP. Channel switch declared per the dedup-search rule; the one dedup search was a single targeted search_issues.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #8181: three other draft-envelope consumers (PermissionMatrixEditor.tsx:445, ObjectHooksPanel.tsx:42-47, StudioDesignSurface.tsx x4) take getDraft().item verbatim with no read-decoration strip — unassigned, unlabelled, not folded into this PR; card states plainly what is measured (no strip anywhere in those files, control: 20/12/55 hits for a token that does exist) and what is NOT (whether a decorated body actually reaches a gate or a write there)"
]
}
Generated by Claude Code
Landed as
0c8dbc492via PR #8182. Closing;pm:dispatchedstripped and the assignee cleared in the same stroke.Verified by CONTENT against a control proven before the merge:
marker pre-merge post-merge stripReadDecorationsinviews/metadata-admin/ResourceEditPage.tsx0 2 (import + call) Lit control for the pre-merge zero: the same symbol was already on main at
packages/app-shell/src/services/MetadataService.ts:19and:679, so the zero was a reading rather than a dark grep. Matched lines were printed, not just counted.⚠️ A correction I owe this card: my dispatch's Zone 1 was falseI told the dev that objectui "already has a correct read-time
_diagnosticsstrip" atMetadataService.ts:658-665. Measured, that is the write path insidesaveFields— its own docblock at:619says the body is stripped "before it is sent" — and it was the only such call in the repo outside a test. No read-time strip existed to reuse.Where the error came from: triage's note said "decoration strip". I added the qualifier "read-time" and turned a true statement into a false one. A dev following my order literally would have gone hunting for something that was not there.
The adjudication survived intact — the reusable asset is the spec's exported
stripReadDecorationshelper, ⛔ never a second hand-maintained key list in objectui, and that is what shipped. But its stated basis was wrong and the dev was right to say so.What shipped is broader than the card asked
The card names one merge site.
extractDraftBodyserves three —:972(load effect),:1443(post-save refresh),:1595(post-publish refresh). Fixing the chokepoint covers all three rather than the one that happened to be reported.Clause-② fired on the accept/reject leg (the client edit gate now accepts bodies it previously rejected) and cleared: the fix removes the framework's own decorations before a re-parse rather than loosening a schema, so parity moves toward the server. The public-face leg did not fire — no export changes,
extractDraftBodyis module-local.⭐ Worth recording: the dev caught a
tsc-only defect in its own pin (Array.findLastabsent from this package's lib target — green under vitest, red undertsc) and replaced it with index arithmetic. That is the class of thing that otherwise lands red in CI after a dev trusts a vitest pass alone.Still open, filed rather than folded in: #8181 — three other draft-envelope consumers take
getDraft().itemverbatim with no strip. I added a fourth to that card while reviewing this PR:extractDraftBodyis implemented three separate times, and after this merge exactly one of them strips.PackageOwdOverviewPanel.tsx:38was not on the original list. Gradedp3, since the strip is provably absent but the consequence is not yet measured.
Generated by Claude Code
What
ResourceEditPage's edit door hands the client Zod gate a draft that still carries the server's own read-time decoration_diagnostics. Every wired metadata type whose schema is.strict()— 14 of the 15, by the count already recorded inResourceEditPage.tsx— then reports a body the server accepts asunrecognized_keys.Found while implementing #6982 (out of that card's scope, which is
clientValidation.ts's two named cures). Filed unassigned for PM triage.The ingress, measured
ResourceEditPage.tsxbuilds the edit baseline from the layered read and then merges the pending draft over it:lay.effectiveis safe:getMetaItemLayeredserves three RAW layers and decoration never reaches it (protocol.ts states this in its own words).draftRealisclient.getDraft(...)'sresp.item, taken verbatim byextractDraftBody. That item is decorated — the strict draft branch returnsitem: decorateMetadataItem(type, ...), anddecorateMetadataItemattaches_diagnosticswhenever the type has a registered Zod schema.Nothing on the objectui side strips it:
validateMetadataDraft(type, draft, ...)receivesdraftas-is.So the misfire needs one condition: the item has a pending draft. Without one,
draftRealis null and the baseline alone is clean — which is why this has stayed invisible.Measured, on the real read path
Driving the real protocol over a real engine (save a
sharing_rule, read it back):And through objectui's own gate, on
flow(a type that gates BOTH doors today):Why the server is the one that is right
The spec names
_diagnosticsa read decoration on purpose, and says a served body "is therefore NOT a valid input to the schema that produced it until these are removed" (kernel/metadata-read-decorations.ts,METADATA_READ_DECORATIONS = ['_diagnostics', '_draft']). The write path strips them, so the same body PUTs back byte-identical. The envelope keys are deliberately excluded from that list — they are allowlisted by the closed schemas precisely so provenance survives a re-parse. The decorations are not.The framework has already paid for this class twice: a served
_diagnostics400ing every saved dataset, and the cold-boot flow bind (cloud#971) throwingunrecognized_keys: ["_diagnostics"]for every flow.Blast radius
Not a Save blocker and nothing is exploitable — schema issues are advisory on this page (
ResourceEditPage.schemaAdvisory.test.tsx), and the server has the last word. The cost is a false "this item is invalid" banner plus inline field errors on a legitimate item, on exactly the workflow where an author is mid-edit._draft: trueis the second decoration and reaches the same place through the preview-draft branch.sharing_ruleis accidentally the only type shielded, becauseAUTHOR_SHAPE_ONLY_TYPESswitches its edit gate off for an unrelated reason — see #6982, where that opt-out is kept for exactly this measurement.Suggested shape, not a decision
Strip the read decorations where the draft is assembled, deriving the key list from the spec's exported
METADATA_READ_DECORATIONSrather than restating it — a second hand-maintained copy of that list in objectui is the thing the spec's own header warns about. ⛔ Not by loosening any schema.Generated by Claude Code