Repository navigation
fix(app-shell): no drafts-ledger read for a member, no published read for a new draft - #26056
Conversation
… for a new draft Home's drafts row now reads GET /meta/_drafts only for a session holding a capability the ledger admits (studio.access, setup.access or manage_metadata), through the new useCanReadPendingDrafts(). The pending-changes sheet's drill-in waits for its new-or-update classification and does not ask a new draft for its published body, which could only answer 404. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fs46jjQVe98Y5YTfDTixa
The sheet's security-posture lint also reads each object draft once its rule module loads, so the exact count is timing-dependent (red on CI Test UI 8/16). The pinned behaviour, no published read for a NEW entry, is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fs46jjQVe98Y5YTfDTixa
席位核对 · ACCEPT · seat
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 40316357014 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 40316532588 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
The pending-changes drill-in now also destructures `isNew` from its computed detail, so the classification flag the load effect reads takes its own name. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fs46jjQVe98Y5YTfDTixa
席位核对 · ACCEPT (re-check) · seat
|
Requested by Steve · project thread
Fixes #25894
Clause-②: no (gating internal reads; no exported type change)
Metadata-protocol: none
What the card saw, and where each request came from
Both requests were located on
origin/mainbefore any change.GET /api/v1/meta/_drafts→ 403 for a member. The only env-wide reader of the ledger a member reaches is Home's "My work" drafts row (usePendingDraftsRowinconsole/home/HomePage.tsx, which calledusePendingDrafts({})for whoever opened Home). The approvals inbox the card walked from lives on Home. No record-page component reads the ledger on main: the other readers (DraftPreviewBarin preview mode, the chat's package-boundPendingDraftsBar, Studio, the metadata editors throughMetadataClient.getDraft) are authoring surfaces.GET /api/v1/meta/object/NAME?package=PKG→ 404 for a draft-only object. That exact URL shape (a by-name read withpackage=and nostate=draft) has one producer inui/:fetchItemBodyinpreview/DraftChangesPanel.tsx, the published-side read of a row's drill-in (EntryDetail). Studio's Publish opens the sheet in the confirm posture, where every row is expanded as the list arrives, so the drill-in read the published body before the per-type new-vs-update read had answered. A NEW item has no published body, so that read could only answer 404.The change
hooks/useCanAuthorMetadata.ts: newPENDING_DRAFTS_CAPABILITIES(studio.access,setup.access,manage_metadata) anduseCanReadPendingDrafts(), "holds any one of them", unknown fails open (the module's existing doctrine). The cohort is the door's own:GET /meta/_draftsasksmayReadPendingDrafts, which isisObjectSchemaMaskExempt, the ADR-0106 D4 cohort. It is deliberately notmanage_metadataalone (the Studio entry's gate), which would have taken the row away from asetup.accessadmin the door admits.console/home/HomePage.tsx: the drafts row reads withusePendingDrafts({ enabled: canReadDrafts }). Held, the row isnull, as with nothing to publish.preview/DraftChangesPanel.tsx: the panel tracks whether the latest load's classification reads are in flight (classifying, keyed by load generation). A drill-in waits while its entry is unclassified and they are pending, and akind: 'new'entry is not asked for its published body. An entry whose classification read failed stays unclassified once they settle and is read both ways, as before.../../../hooks/useCanAuthorMetadatawith an explicit factory; each gainsuseCanReadPendingDrafts: () => true, which keeps them on their previous behaviour.Tests
Both new files drive the real code path: the Home test mounts the real
MePermissionsProviderover a/me/permissionspayload, and the panel test runs the real panel against a recordingfetch.HomePage.draftsReadGate-25894.test.tsx: a member with a reported set holding none of the three gets no_draftsread and no row; a reported empty set gets no read; CONTROLS: each ofsetup.access,studio.access,manage_metadataalone reads once and renders the row; an unreported set fails open and reads.DraftChangesPanel.newEntryNoPublishedRead-25894.test.tsx: in the confirm posture a NEW entry sends only its draft read; CONTROL: an UPDATE entry still reads its published body (withpackage=); while the published-list read is held, no drill-in sends a published read; CONTROL: a failed classification read leaves both entries read both ways.Run, under the shared verify lock, from
ui/:vitest run --maxWorkers=2 packages/app-shell/src/preview/__tests__/DraftChangesPanel packages/app-shell/src/console/home/__tests__/HomePage. Result: 29 files, 186 tests, all pass. This covers everyDraftChangesPanelandHomePagetest file. The full app-shell package is left to CITest Core, a declared narrowing.Ablation: both fixed files were set back to
origin/main(the hook left in place), and the two new files were re-run. The 4 headline cases went red: no-published-read, held-classification, member-no-read, empty-set-no-read. The 5 controls stayed green. The files were then restored withgit checkout HEAD, andgit diff HEADwas empty afterwards. The tests import app-shell source directly, so no dist rebuild sits between the mutation and the run.Derived gates
pnpm --filter @object-ui/app-shell type-check(bothtsc --noEmitandtsc -p tsconfig.test.json), afterpnpm --filter '@object-ui/app-shell^...' build: exit 0, zero errors, at head e32db46.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 35 commands. All exited 0,node scripts/check-sdui-manifest.mjsincluded.--ranreconciliation: 34 derived families accounted for, 34 run, 0 NOT-MEASURED, each with an exit code.node scripts/gen-sdui-manifest-node.mjswas re-run. The registry sources hash is unchanged, because app-shell is not one of the 27 registry packages, soscripts/sdui-manifest.record.jsonneeds no change and is not in this diff.Acceptance notes
@objectstack/client'smeta.listDraftsandMetadataClient.getDraft's ledger probe also send an unfilteredGET /meta/_drafts. Every caller of either on main sits behind an authoring surface, so neither is gated here.DraftChangesPanel's per-member classification (email_templatemembers) still reads the by-name door with?member=and gets a 404 for a new member. That read is how the sheet learns the member is new, so it stays. Its drill-in is now spared the second 404.DraftChangesPanel.tsx, incomputeChangeDetailand the drill-in's render, not the load effect or the panel state this PR touches. ItsisNew = published === nullreads the same answer when the published read is skipped. This branch mergesorigin/mainonce fix(app-shell): pending changes folds platform-written system fields and derived keys into one collapsed line #25984 lands.🤖 Generated with Claude Code
https://claude.ai/code/session_011fs46jjQVe98Y5YTfDTixa
Generated by Claude Code