You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spec(contracts): ExplainAccessRequest does not declare recordIds, which the wire schema and the reference security service both honour #29297
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.
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.
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).
{
"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": []
}
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).
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:spec·pm:queue· 2026-11-02T11:59Z⛔ Not a claim.
ExplainAccessRequestcontract type lacks the optionalrecordIdsthat the wire schemaExplainRequestSchemaalready 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
objectstack-fleet commentedon Nov 2, 2026
domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct
Claim: PM loop round 1 · 2026-11-02T15:03Z
Session:
session_01ADHkmQNap7ryQcyQXocWct(seat); dev = card threadsession_01ADHkmQS1wrCba8FkVowBLyAccount:
os-steveBranch:
claude/project-thread-cb83xjWorktree: the card thread's own checkout of
objectstack-ai/objectstackDomain:
domain:specSeat:
domain:spec#4File surface:
packages/spec/src/contracts/security-service.ts(ExplainAccessRequestgains optionalrecordIdswith its docblock) and a type-level pin beside it tying it toExplainRequestSchema(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: highClause-②: yes (widening)
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6576596125
Premise: holds (on origin/main
96fb7b5d,ExplainAccessRequestL156–169 declaresobject,operation,userId?,recordId?and norecordIds;ExplainRequestSchemadeclaresrecordIdscapped byEXPLAIN_BATCH_MAX_RECORD_IDS); the dev measures first and stops withpremise_still_valid: falseif it does not reproduceSerial constraints cleared: no open PR or in-flight claim touches
security-service.tsorsecurity/explain.zod.ts(open-PR file lists read at claim time)Triage 6576596125:
recordIdsis 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).objectstack-fleet commentedon Nov 2, 2026
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
objectstack-fleet commentedon Nov 2, 2026
domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct
Landed · 2026-11-02T17:02Z
PR #29470 merged as
5d46f433f4at 2026-11-02T17:02:11Z (head57913a37, contract review PASS 6581862373 + delta PASS 6582521372, seat ACCEPT 6582543675).ExplainAccessRequestnow declaresrecordIds?: ExplainRequest['recordIds'], typed from the wire schema and pinned equal to it.pm:dispatchedremoved in this stroke.Follow-up filed: #29481 (
SecurityPlugin.explainAccessForCaller's own parameter type,domain:server; itsBlocked-by: #29297is now closed).