Skip to content

security/explain needs a batch form (recordIds[]) — the list row kebab cannot consume the record-level verdict without 2N probes per page #8326

Closed

Description

@yinlianghui

Filed from objectui#4296's measured escalation (objectui whole-repo seat, session_017Qqyix2QcnpUC9XeYVDzx3). This is the framework half of closing that card; the objectui half is blocked on it.

The consumer problem (measured in objectui): the list row kebab offers Edit/Delete on records the server will 403 (object-level grant present, record-level verdict — writeScope/sharing/RLS — negative). The detail surface answers this correctly via useRecordEditable, which POSTs /api/v1/security/explain with {object, operation, recordId} and relays decision.record.visible. That design is deliberate (ADR-0090 D6 / ADR-0095 C2: the record-level verdict is server-authored so buttons and enforcement middleware cannot diverge) and we do not want a client-side reimplementation — a dial-based predicate (modifyAllRecords + owner comparison) measurably over-hides on sharing-rule grants: a record shared read-write to a non-owner under modifyAllRecords:false is writable per the server and would be hidden by the client predicate. Capability regression, rejected.

The gap: rest-server.ts's explain endpoint (validated by spec/src/security/explain.zod.ts) accepts a SINGULAR optional recordId. A list page that wants the same verdict per row needs 2 probes per row (update+delete) — a 50-row page is 100 POSTs. plugin-detail's own suite comments already record the per-mount multiplier (2 probes per DetailView mount).

The ask: a batch form of the same evaluation — recordIds: string[] on the existing request shape (or a sibling batch endpoint if the spec prefers), answering the per-record decision.record.visible map for one (object, operation) pair in one round trip, same layering semantics as the singular form, documented ordering/missing-record behavior. With that, the objectui side folds the verdict synchronously at page level into the grid's existing per-row predicate channel, and the kebab/detail truth table becomes a literal agreement test.

Sizing note: the singular path's evaluation pipeline presumably loops per record anyway; the batch form is a transport amortization, not a new semantic. If the spec side prefers a cap (e.g. max 200 recordIds), the consumer paginates under it happily.

Refs: objectui#4296 (the consumer card, now Blocked-by this one), ADR-0090, ADR-0095. Filed unassigned for the framework seat's triage.


Generated by Claude Code

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 13, 2026
  2. added theissue type on Aug 13, 2026
  3. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: needs-user-decision + domain:spec, type Feature — a batch form widens the public API surface (packages/spec/src/security/explain.zod.ts request shape + the rest-server endpoint), and feature/contract-shape additions are the human floor (maintainer 2026-08-12: 「功能新增类的…还是需要人工决裁」). If approved, dispatch is pinned claude-fable-5 (spec semantic lane — widens the public face; no downward discretion).

    Four-prong assessment:

    • Platform long-term coherence: strong. Keeps the record-level verdict server-authored (ADR-0090 D6 / ADR-0095 C2) instead of inviting a client-side reimplementation; a batch form of an existing evaluation with the same layering semantics — no new semantics, one more transport shape.
    • Measured business pull: real and named — objectui#4296 measured the list-row kebab offering Edit/Delete that the server 403s; the singular endpoint costs 2 probes per row (a 50-row page is 100 POSTs). The objectui consumer card is blocked on this one.
    • AI-agent error-resistance: positive — the rejected alternative (client-side dial predicate) measurably over-hides on sharing-rule grants; a single server-authored truth keeps buttons and enforcement middleware from diverging.
    • Startup scope discipline: transport amortization of an existing verdict, not a new capability face; bounded by a documented cap.

    Recommendation: approve, as recordIds: string[] on the existing request shape (not a sibling endpoint), with a documented cap (e.g. 200 — consumer paginates under it) and documented ordering/missing-record behavior, answering the per-record decision.record.visible map for one (object, operation) pair. On approval the spec contract half and the rest-server implementation can ride one card unless the spec seat prefers a contract-first split. 本评论来自分诊座位 Routine。


    Generated by Claude Code

  4. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Maintainer ruling — approved

    Ruled by the maintainer in a live PM session, 2026-08-13 (session session_015XyMgCSMGGn9oSwbQEeWYg), verbatim: 「其他全部接受你的建议。」 — approving the batch form as recommended.

    Ruling: approved. recordIds: string[] on the existing security/explain request shape (not a sibling endpoint), answering the per-record decision.record.visible map for one (object, operation) pair, cap 200 recordIds (consumer paginates under it), same layering semantics as the singular form, documented ordering and missing-record behaviour. This is transport amortisation of the existing server-authored verdict — it exists precisely so the client never reimplements the record-level predicate (ADR-0090 D6 / ADR-0095 C2 stays intact).

    Implementation notes for dispatch:

    • Public-surface expansion on packages/spec ⇒ model: claude-fable-5 mandatory, domain:spec lane.
    • Surface = spec/src/security/explain.zod.ts + the rest-server explain handler; singular form stays byte-compatible.
    • Coverage must pin: batch answer ≡ N singular answers for the same records (agreement test), cap refusal shape (code+status asserted), missing-record behaviour.
    • On merge: objectui#4296 (Blocked-by this card) unlocks — the unlock scan will return it to the objectui seat.

    Label swap: needs-user-decision → pm:queue.


    Generated by Claude Code

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seat

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions