Skip to content

fix(app-shell): no drafts-ledger read for a member, no published read for a new draft - #26056

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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/main before any change.

  1. 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 (usePendingDraftsRow in console/home/HomePage.tsx, which called usePendingDrafts({}) 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 (DraftPreviewBar in preview mode, the chat's package-bound PendingDraftsBar, Studio, the metadata editors through MetadataClient.getDraft) are authoring surfaces.
  2. GET /api/v1/meta/object/NAME?package=PKG → 404 for a draft-only object. That exact URL shape (a by-name read with package= and no state=draft) has one producer in ui/: fetchItemBody in preview/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: new PENDING_DRAFTS_CAPABILITIES (studio.access, setup.access, manage_metadata) and useCanReadPendingDrafts(), "holds any one of them", unknown fails open (the module's existing doctrine). The cohort is the door's own: GET /meta/_drafts asks mayReadPendingDrafts, which is isObjectSchemaMaskExempt, the ADR-0106 D4 cohort. It is deliberately not manage_metadata alone (the Studio entry's gate), which would have taken the row away from a setup.access admin the door admits.
  • console/home/HomePage.tsx: the drafts row reads with usePendingDrafts({ enabled: canReadDrafts }). Held, the row is null, 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 a kind: '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.
  • Four existing Home tests mock ../../../hooks/useCanAuthorMetadata with an explicit factory; each gains useCanReadPendingDrafts: () => true, which keeps them on their previous behaviour.

Tests

Both new files drive the real code path: the Home test mounts the real MePermissionsProvider over a /me/permissions payload, and the panel test runs the real panel against a recording fetch.

  • HomePage.draftsReadGate-25894.test.tsx: a member with a reported set holding none of the three gets no _drafts read and no row; a reported empty set gets no read; CONTROLS: each of setup.access, studio.access, manage_metadata alone 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 (with package=); 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 every DraftChangesPanel and HomePage test file. The full app-shell package is left to CI Test 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 with git checkout HEAD, and git diff HEAD was 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 (both tsc --noEmit and tsc -p tsconfig.test.json), after pnpm --filter '@object-ui/app-shell^...' build: exit 0, zero errors, at head e32db46.

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 35 commands. All exited 0, node scripts/check-sdui-manifest.mjs included. --ran reconciliation: 34 derived families accounted for, 34 run, 0 NOT-MEASURED, each with an exit code.

node scripts/gen-sdui-manifest-node.mjs was re-run. The registry sources hash is unchanged, because app-shell is not one of the 27 registry packages, so scripts/sdui-manifest.record.json needs no change and is not in this diff.

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_011fs46jjQVe98Y5YTfDTixa


Generated by Claude Code

… 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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 26, 2026
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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-26T10:56Z

Head c91002477a0b (read from the PR object).

  • Shape. Draft on main; Fixes #25894; changeset .changeset/25894-no-drafts-request-without-grant.md in the root .changeset/ (patch, app-shell). Every file is inside app-shell, as the claim required.
  • Checks pinned to the head. 54 named check runs, latest per name: 46 success, 8 skipped, 0 failing, 0 pending. Head c9100247 is a test-only commit. It relaxes the panel test's exact draft-read count to "at least one draft read, zero published reads", because the security-posture lint also reads object drafts after its rule module loads. No product code changed after the os-dev-report.
  • Governed surface. check-governed-merges.mjs --pr 26056: 0 of 10 paths are governed, 397 changed lines.
  • Fix, as triage ruled.
    • Home's drafts row reads GET /meta/_drafts only when useCanReadPendingDrafts() admits the session. Its capability set (studio.access, setup.access, the authoring capability) mirrors the door's mayReadPendingDrafts cohort, so a setup.access admin keeps the row. Unknown fails open, as useCanAuthorMetadata does.
    • In the Studio Publish sheet, EntryDetail skips the published read for a kind: 'new' entry. It waits while the entry is unclassified and the classification reads are still in flight, keyed by load generation. An entry still unclassified once those reads settle is read both ways, as before.
  • Overlap with fix(app-shell): pending changes folds platform-written system fields and derived keys into one collapsed line #25984. Both PRs touch DraftChangesPanel.tsx, in different hunks. The seat ran git merge-tree of this head against fix(app-shell): pending changes folds platform-written system fields and derived keys into one collapsed line #25984's head and against origin/main: both are clean.
  • sdui record. Unchanged, because app-shell is outside the registry closure. Not in the hash serial queue.

Landing: ready + auto-merge through the relay.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40316357014 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Build Core — 失败步骤: Build packages (excluding docs)(日志不可读,点进 job 看)

  • Lint UI — 失败步骤: Build every ui package the gates below read(日志不可读,点进 job 看)

  • Type Check UI — 失败步骤: Run type-check over the ui packages(日志不可读,点进 job 看)

  • Build & E2E UI — 失败步骤: Build the Console for E2E

    ✗ Build failed in 5.59s
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 21 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40316532588 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Build Core — 失败步骤: Build packages (excluding docs)(日志不可读,点进 job 看)

  • Lint UI — 失败步骤: Build every ui package the gates below read(日志不可读,点进 job 看)

  • Type Check UI — 失败步骤: Run type-check over the ui packages(日志不可读,点进 job 看)

  • Build & E2E UI — 失败步骤: Build the Console for E2E

    ✗ Build failed in 7.07s
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 23 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (re-check) · seat domain:ui#1 · 2026-10-26T12:38Z

Head d4b5ee12186b (read from the PR object). It supersedes the ACCEPT on c91002477a0b.

Landing: auto-merge re-armed through the relay.

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/m tooling

Projects

None yet

2 participants