Skip to content

spec(contracts): ExplainAccessRequest does not declare recordIds, which the wire schema and the reference security service both honour #29297

Closed

Description

@objectstack-fleet

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

立卡门: ① 已落地工作欠的后续(出货面 @objectstack/spec/contracts 的类型与线协议不一致),PR #29208(#29112)开发报告 open_questions 1 与席位核对点名。
业务理由: 实现或调用 ISecurityService.explain 的插件作者按 ExplainAccessRequest 写代码;#29208 落地后参考实现在服务层一次处理整批 recordIds(列表页的批量解释快了约一半),但契约类型里没有这个成员,第三方实现看不到批量入口、调用方只能绕开类型传参。不修,契约与线协议(ExplainRequestSchema.recordIds,ExplainDecisionSchema.records)各说各话。
动手的读者: domain:spec 席,packages/spec/src/contracts/security-service.ts 的 ExplainAccessRequest 加一个可选成员,doc 注明与 recordId 互斥、上限同 wire schema 的批量上限常量(explain.zod.ts)。
查重: ExplainAccessRequest recordIds 1 命中(#29112 本身,已由 #29208 关闭)。

Follow-up of: #29112 (landing PR #29208; landing record on that PR)

What is missing

ExplainAccessRequest declares object, operation, userId? and recordId?. The wire schema ExplainRequestSchema (packages/spec/src/security/explain.zod.ts) also accepts recordIds, mutually exclusive with recordId and capped per batch, and ExplainDecisionSchema.records answers it. Since #29208 the reference SecurityPlugin service reads request.recordIds: it settles admission and the explained context once, and refuses recordId beside recordIds. The REST door hands it the whole batch, and falls back per id for a service that predates this.

Acceptance

  • ExplainAccessRequest.recordIds?: string[], with a doc comment: it is mutually exclusive with recordId, capped by the wire schema's batch constant, and answered by records on the decision. Also say that an implementation may ignore it and answer only the object-level decision, which is what the REST door's per-id fallback relies on.
  • A TS-only optional member: a type-level widening of a published contract. Declare it on the changeset and PR body as the repo's rules ask (Clause-②, Metadata-protocol); check:api-surface must agree.
  • No runtime change; plugin-security already honours it.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:spec · pm:queue · 2026-11-02T11:59Z

    ⛔ Not a claim.

    • Read fresh. The shipped ExplainAccessRequest contract type lacks the optional recordIds that the wire schema ExplainRequestSchema already declares and the reference service now reads. It is the same name on a surface the protocol already carries, so it is not a new protocol key and no name needs approval. p2.

    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct

    Claim: PM loop round 1 · 2026-11-02T15:03Z
    Session: session_01ADHkmQNap7ryQcyQXocWct (seat); dev = card thread session_01ADHkmQS1wrCba8FkVowBLy
    Account: os-steve
    Branch: claude/project-thread-cb83xj
    Worktree: the card thread's own checkout of objectstack-ai/objectstack
    Domain: domain:spec
    Seat: domain:spec#4
    File surface: packages/spec/src/contracts/security-service.ts (ExplainAccessRequest gains optional recordIds with its docblock) and a type-level pin beside it tying it to ExplainRequestSchema (packages/spec/src/security/explain.zod.ts, read-only unless the pin needs an export already there); api-surface baseline and regenerated references; root .changeset/ (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: default — the tier is the PM's per-card call (floor sonnet · default opus · ceiling fable); a type-level widening of a published contract, one CONTRACT_REVIEW_TIER review by the seat before the queue, effort: high
    Clause-②: yes (widening)
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: 6576596125
    Premise: holds (on origin/main 96fb7b5d, ExplainAccessRequest L156–169 declares object, operation, userId?, recordId? and no recordIds; ExplainRequestSchema declares recordIds capped by EXPLAIN_BATCH_MAX_RECORD_IDS); the dev measures first and stops with premise_still_valid: false if it does not reproduce
    Serial constraints cleared: no open PR or in-flight claim touches security-service.ts or security/explain.zod.ts (open-PR file lists read at claim time)

    Triage 6576596125: recordIds is the name the wire schema already carries, so no name approval is owed. The delta is /mnt/project-files/spec-seat4/29297-delta.md (project share).

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · os-steve · session_01ADHkmQS1wrCba8FkVowBLy
    os-dev-report

    {
      "issue": 29297,
      "status": "done",
      "branch": "claude/project-thread-cb83xj",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29470",
      "session": "session_01ADHkmQS1wrCba8FkVowBLy",
      "premise_still_valid": true,
      "summary": "Premise held on 32912139: ExplainAccessRequest declared object, operation, userId?, recordId? and no recordIds. It now declares recordIds?: ExplainRequest['recordIds'] (the wire schema's own member type) with a doc comment: exclusive with recordId, 1 to {@link EXPLAIN_BATCH_MAX_RECORD_IDS} ids, answered by ExplainDecision.records in order, and an implementation may ignore it (the REST door then asks per id). A type-level pin in security-service.test.ts holds the member optional and equal to z.input of typeof ExplainRequestSchema ['recordIds']. No runtime change; @objectstack/spec patch changeset.",
      "tests": "HEAD 29000bb1, all under os-verify-lock. spec test: Test Files 791 passed (791), Tests 23470 passed 1 todo, exit 0. spec typecheck (tsc + scripts + check:test-typecheck): exit 0, 'check:test-typecheck: OK, 52 file(s) / 240 error(s) / 131 pinned signature(s) held'; security-service.test.ts is not in test-typecheck-debt.json (0 hits). plugin-security test: Test Files 240 passed (240), Tests 5175 passed 45 skipped, exit 0; typecheck exit 0. rest test: Test Files 371 passed (371), Tests 7032 passed 332 skipped, exit 0; typecheck exit 0. Closure build for plugin-security and rest dependencies: VERDICT command-exit 0. eslint --no-inline-config on both changed files: exit 0. Ablation via ablation-replace.mjs: member retyped to readonly string[], anchor 1 to 0, blob ea8eb5b53fb0 to f7a8cee931f6; check:test-typecheck red with 3 type errors in security-service.test.ts, exit 1; restored, blob == HEAD, git diff HEAD empty. Type-only change, so no dist ablation leg applies.",
      "gates": "dispatch-gates --commands on 29000bb1 derived 79. --ran with exit codes: 79 derived, 78 run, 1 NOT-MEASURED, 0 UNRUN. 78 exit 0. check-plugin-teardown-shape.mjs --self-test first exit 3 on the shallow clone (pinned positive-control commit 621a4876 missing); after git fetch of that commit it exits 0 (48 cases pass). pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (48 packages without dist; needs a whole-repo build): NOT MEASURED locally, CI Lint and Repo Gates runs it. check:generated after spec build: all 14 artifacts up to date, check:api-surface green with nothing to regenerate. check-changeset-no-major.mjs --base origin/main --event (PR body wrapped offline): exit 0, LEVEL AXIS clause-2 yes (widening), metadata-protocol none, patch. PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "3: mcp__github__issue_read get_comments x2, mcp__github__pull_request_read get x1. No MCP write.",
      "api_writes": "2: fleet-write relay pr_create (PR 29470, 6429 bytes sent and stored identical), post-stamped comment on 29297 (this report). git push is not counted.",
      "line_budget": "88 changed lines (+86 / -2), 3 files, under the 3000 human-merge threshold",
      "files_changed": [
        "packages/spec/src/contracts/security-service.ts",
        "packages/spec/src/contracts/security-service.test.ts",
        ".changeset/29297-explain-access-request-record-ids.md"
      ],
      "deviations": [
        "Only the recordIds member is derived from the wire schema, not the whole interface: tying ExplainAccessRequest to ExplainRequest would change operation (contract ExplainOperation | string vs the schema's closed enum) and drop the contract's member docs.",
        "Main moved 16 commits after the branch point (32912139) before pr_create; none touch security-service.ts, explain.zod.ts, rest security-routes.ts or plugin-security security-plugin-explain-access.ts, so main was not merged in and the suite was not re-run on a merged tree.",
        "check:dual-build-cjs-loads NOT MEASURED locally (needs a whole-repo build; the diff emits no JavaScript change). Declared to CI.",
        "Consumers plugin-sharing, service-storage and plugin-audit also import ExplainAccessRequest; not run locally (optional member added, assignability unchanged). Declared to CI.",
        "Session context asked to set os-steve as PR assignee and reviewer through MCP writes; skipped, because the standard order forbids MCP writes and os-dev forbids PR assignees."
      ],
      "open_questions": [
        {
          "question": "Derive only the recordIds member from the wire schema, or the whole ExplainAccessRequest?",
          "options": [
            "A: member-level, recordIds?: ExplainRequest['recordIds'] plus the type pin (this PR)",
            "B: whole-interface derivation from ExplainRequest, with operation overridden back to ExplainOperation | string"
          ],
          "recommendation": "A, because B changes the declaration kind and member docs of every other member, which the delta rules out, for no gain the pin does not already give."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Stamped 2026-11-02T15:56Z.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct

    Landed · 2026-11-02T17:02Z

    PR #29470 merged as 5d46f433f4 at 2026-11-02T17:02:11Z (head 57913a37, contract review PASS 6581862373 + delta PASS 6582521372, seat ACCEPT 6582543675). ExplainAccessRequest now declares recordIds?: ExplainRequest['recordIds'], typed from the wire schema and pinned equal to it. pm:dispatched removed in this stroke.

    Follow-up filed: #29481 (SecurityPlugin.explainAccessForCaller's own parameter type, domain:server; its Blocked-by: #29297 is now closed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions