Skip to content

feat(spec): ExplainAccessRequest declares recordIds, the batch selector the explain wire schema already carries - #29470

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-cb83xj
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-cb83xj

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29297

Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: none — no page under skills/ or content/docs/ spells the members of the ExplainAccessRequest contract type; content/docs/permissions/explain.mdx shows the kernel call with object/operation/userId only, and the batch form is already a wire-schema fact of ExplainRequestSchema (grep explain and recordIds in both trees, on 29000bb).

维护者速读

  • 改了什么:security 服务契约的 explain 请求类型 ExplainAccessRequest 补上可选的 recordIds(批量记录 id),类型直接取自线上 schema ExplainRequestSchema 的同名成员。
  • 为什么改:线上 schema、REST 解释路由和参考安全服务早已支持批量形态,唯独它们之间的 TypeScript 契约没声明,调用方只能强转才能传批量请求。
  • 风险与回滚:仅类型放宽,无运行时改动;此前能编译的调用照常编译。回滚即撤销本 PR。
  • 先看:packages/spec/src/contracts/security-service.ts(新成员与文档注释)、packages/spec/src/contracts/security-service.test.ts(类型钉)。
  • 两分钟验证:pnpm --filter @objectstack/spec check:test-typecheck,预期 OK;把新成员改成 readonly string[] 再跑,预期该测试文件报 3 个类型错误。
  • 需要你做的:无,席位落地。

What changes

  • ExplainAccessRequest (packages/spec/src/contracts/security-service.ts) gains recordIds?: ExplainRequest['recordIds'], with a doc comment: mutually exclusive with recordId; at least one and at most EXPLAIN_BATCH_MAX_RECORD_IDS ids (linked, not spelled); answered by ExplainDecision.records in the same order; an implementation may ignore it and answer only the object-level decision, and the caller then asks each id singularly, as the REST explain route does.
  • The member's type is the wire schema's own (ExplainRequest = z.input<typeof ExplainRequestSchema>), not a hand-written string[]. Only this member is derived: tying the whole interface to the schema would change operation (the contract's ExplainOperation | string against the schema's closed enum) and replace the contract's member docs.
  • The last doc sentence is measured against both readers on 29000bb: the REST door hands the service the batch and falls back per id when the answer carries no records of the right length (packages/rest/src/security-routes.ts L256–299, the call at L279, the check at L280, the per-id loop at L289–296); the reference service reads the member and refuses it beside recordId (packages/plugins/plugin-security/src/security-plugin-explain-access.ts L128, L142–145, L526–527).
  • No runtime change. check:api-surface agrees with no regeneration (it records export names; no export was added or removed).
  • Changeset: .changeset/29297-explain-access-request-record-ids.md, @objectstack/spec patch.

Tests

  • New in packages/spec/src/contracts/security-service.test.ts (no existing test was edited):
    • ExplainRecordIdsIsOptional: a type alias that reds if the member becomes required.
    • ExplainRecordIdsMatchesWire: a type alias holding ExplainAccessRequest['recordIds'] equal to z.input<typeof ExplainRequestSchema>['recordIds'].
    • explain accepts a batch request…: the same equality through expectTypeOf, a batch literal reaching the service, and a @ts-expect-error that a bare string is not a batch.
  • The file is compiled by check:test-typecheck (tsconfig.test.json) and is not in test-typecheck-debt.json (0 hits), so the aliases are live checks.
Ablation (type pin)

node scripts/ablation-replace.mjs --file packages/spec/src/contracts/security-service.ts --anchor "recordIds?: ExplainRequest['recordIds'];" --replacement 'recordIds?: readonly string[];' -- pnpm --filter @objectstack/spec check:test-typecheck

  • mutation landed: anchor 1 → 0, blob ea8eb5b53fb0 → f7a8cee931f6
  • src/contracts/security-service.test.ts: 3 type error(s) in a file the ledger does not cover · command exited 1
  • restored: blob == HEAD (ea8eb5b53fb0), git diff HEAD empty
Package suites on 29000bb (os-verify-lock)
  • closure build --filter '@objectstack/plugin-security^...' --filter '@objectstack/rest^...': VERDICT command-exit 0
  • @objectstack/spec test: Test Files 791 passed (791) · Tests 23470 passed, 1 todo · exit 0
  • @objectstack/spec typecheck (tsc + scripts + test layer): exit 0 · check:test-typecheck: OK — 52 file(s) / 240 error(s) / 131 pinned signature(s) held
  • @objectstack/plugin-security test: Test Files 240 passed (240) · Tests 5175 passed, 45 skipped · exit 0
  • @objectstack/plugin-security typecheck: exit 0
  • @objectstack/rest test: Test Files 371 passed (371) · Tests 7032 passed, 332 skipped · exit 0
  • @objectstack/rest typecheck: exit 0
  • eslint --no-inline-config on both changed files: exit 0

Acceptance notes

  • @objectstack/plugin-sharing, @objectstack/service-storage and @objectstack/plugin-audit also import ExplainAccessRequest; the change only adds an optional member, so their code and test doubles stay assignable. Not run locally; CI's Test Core and TypeScript Type Check cover them.

Verification

  • Gates on 29000bb: dispatch-gates.mjs --commands derived 79; --ran reconciles 79 derived, 78 run, 1 NOT-MEASURED, 0 UNRUN. 78 exit 0.
    • check-plugin-teardown-shape.mjs --self-test first exited 3 on this shallow clone (its positive-control fixture is pinned to a commit the clone lacked); after fetching that one commit it exits 0 (48 cases pass).
    • pnpm check:dual-build-cjs-loads exits 3, PREREQUISITE NOT MET (48 packages have no dist/; it needs a whole-repo build). NOT MEASURED locally; this diff changes one declared type member and no emitted JavaScript. CI's Lint & Repo Gates runs it.
  • check-changeset-no-major.mjs --base origin/main --event against this body, offline: exit 0, LEVEL AXIS … clause-② yes (widening) and metadata-protocol none … a non-protocol change at patch.

Generated by Claude Code

…s batch selector

The service contract the REST explain route calls now declares the optional
batch selector the wire schema (ExplainRequestSchema) and the reference
security service already honour. The member's type is the wire schema's own,
and a type-level pin holds the two equal.

Claude-Session: https://claude.ai/code/session_01ADHkmQS1wrCba8FkVowBLy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via recordIds (symbol, a field of interface ExplainAccessRequest), recordIds (literal, a string literal in recordIds))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6af3d295757186a6e5299b4004d1810a9d144765 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3d62871e06a879c774bb36baa255a70d7180067c — the merge of head 57913a379525abcbfb63ab4fba331fda9b2ce136 into base 6af3d295757186a6e5299b4004d1810a9d144765, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d62871e06a879c774bb36baa255a70d7180067c && git checkout 3d62871e06a879c774bb36baa255a70d7180067c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6af3d295757186a6e5299b4004d1810a9d144765 57913a379525abcbfb63ab4fba331fda9b2ce136 && git checkout -B drift-repro 6af3d295757186a6e5299b4004d1810a9d144765 && git merge --no-ff 57913a379525abcbfb63ab4fba331fda9b2ce136

node scripts/docs-audit/affected-docs.mjs --json 6af3d295757186a6e5299b4004d1810a9d144765

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6af3d295757186a6e5299b4004d1810a9d144765 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct

Contract-review-claim: session_01ADHkmQNap7ryQcyQXocWct · head 29000bb · 2026-11-02T15:57Z

The lane seat reviews this head itself at CONTRACT_REVIEW_TIER (triage ruling 6575141496 on #28741 returned contract review to the lane): ExplainAccessRequest (a published contract type) gains the optional recordIds member typed from the wire schema.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 29000bb116f9b5f488199006631493dc75798cbe
Local-runs: none
Scope: packages/spec/src/contracts/security-service.ts + packages/spec/src/security/explain.zod.ts, packages/rest/src/security-routes.ts, packages/plugins/plugin-security/src/security-plugin-explain-access.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/security-plugin-security-service.ts, packages/plugins/plugin-security/src/explain-engine.ts, packages/plugins/plugin-sharing/src/sharing-service.ts, packages/spec/src/data/masked-field-types.ts (precedent), packages/spec/scripts/build-api-surface.ts, packages/spec/api-surface/contracts.json, tsconfig.base.json. The changeset and security-service.test.ts are the lane seat's ACCEPT at TIER_DEFAULT.

Inputs read: card #29297 (body, comments 6576596125, 6580454301, 6581669782), PR #29470 body and file list, git diff origin/main...29000bb1 (3 files, +86/-2), the head's check-runs at 2026-11-02T16:02Z.

① Derived judgments

  1. Caller-side accept set widens, nothing narrows — right. ISecurityService.explain(request) (L1130) now accepts an object literal carrying recordIds without an excess-property error; every request that compiled on main (object, operation, userId?, recordId?) still compiles. The member is optional (recordIds?: ExplainRequest['recordIds'], L182).
  2. Implementer side stays assignable — right. The only ISecurityService implementer in the tree is security-plugin-security-service.ts L344, which spreads the request into SecurityPlugin.explainAccessForCaller; the extracted body (security-plugin-explain-access.ts L128) already types the member readonly string[], which accepts the contract's string[]. The structural slice in plugin-sharing/sharing-service.ts L479 takes ExplainAccessRequest itself; the vi.fn doubles in plugin-audit and service-storage take the contract type; the plugin-webhooks testkit stub declares a narrower parameter, which method bivariance still admits. No consumer is moved off the contract.
  3. Public surface: no export added, removed or renamed — right. build-api-surface.ts records one name (kind) row per declared kind (header L13–16; members are ungated, L40), so the ExplainAccessRequest (interface) row in api-surface/contracts.json L108 is unchanged and no regeneration is owed. The dev's check:generated report and the green Type Check · source gates run on the head agree.
  4. Member type derived from the wire schema, not hand-spelled — right, and the right side. ExplainRequest is z.input of ExplainRequestSchema (explain.zod.ts L325), the caller-written shape, which is what a caller-side request type must mirror. For this member the choice is also immaterial: recordIds carries no .default() or .transform() (L303), so input and output are both string[] | undefined. The test pin spells z.input explicitly. Deriving ONLY this member is correct: the contract's operation is deliberately ExplainOperation | string (L160) against the schema's closed enum, so a whole-interface derivation would be a narrowing.
  5. import type { EXPLAIN_BATCH_MAX_RECORD_IDS } (a const imported type-only, used in a {@link}) — sound under this repo's config. TypeScript admits a type-only import of a value declaration; a {@link} reference satisfies noUnusedLocals (tsconfig.base.json L24); the base config sets neither verbatimModuleSyntax nor isolatedModules, and the root eslint config carries no consistent-type-imports or no-unused-vars rule. The pattern already ships on main: packages/spec/src/data/masked-field-types.ts L70 imports SECRET_MASK type-only and references it only in {@link SECRET_MASK} (L9, L106). Lint · ESLint and Type Check · source gates are green on the head. Emitted d.ts: declaration emit keeps an import specifier only when an emitted declaration references it, so the JSDoc-only specifier may be elided from the bundled contracts/index.d.ts and the {@link} text then resolves nowhere in a consumer's hover — cosmetic, identical to the SECRET_MASK precedent, and invisible to the api-surface gate, which records names. Not a defect.
  6. The doc comment's claims hold against the cited code. Mutual exclusion: wire superRefine at explain.zod.ts L313–323; reference refusal at security-plugin-explain-access.ts L142–145. Cap and min: L303 (.min(1).max(EXPLAIN_BATCH_MAX_RECORD_IDS), constant = 200 at L257). Ordering: ExplainDecisionSchema.records L419–430; engine explain-engine.ts L2510 records: recordIds.map(...) — same length, per position, duplicates per position; REST L280–285 relays and fail-closes a missing position. "An implementation may ignore it; the REST door then asks per id": security-routes.ts L279 call, L280 length check, L288–296 per-id loop. The PR body's line citations (L256–299, L279, L280, L289–296; L128, L142–145, L526–527) are accurate on 29000bb.

② Semver level

  • Changeset .changeset/29297-explain-access-request-record-ids.md: @objectstack/spec: patch, Clause-②: yes (widening), Metadata-protocol: none; the PR body carries the same two lines. Right: an optional member on a published contract interface widens the accepted set and refuses nothing; no authorable metadata key, value or default moves (recordIds is a wire-schema member the protocol already carried, as triage 6576596125 ruled, not a new protocol key), so none takes patch under Post-Task Checklist Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 (a non-protocol widening ships patch). Only @objectstack/spec moves; no runtime package changes. Check Changeset is green on the head.
  • The changeset's statements ("a type-level test holds the two equal", "every request that compiled before still compiles", "no runtime change") are each true of the tree.

③ Boundary flags

  • Dev deviation 1 (member-level derivation, not whole-interface) — accepted; see ①4. Whole-interface derivation would turn operation into the closed enum, a narrowing the card did not order.
  • Dev deviation 2 (main moved 16 commits after the branch point; not merged in) — verified: git log 32912139..origin/main on the contract, its test, explain.zod.ts, the REST route, the three plugin-security files and api-surface/contracts.json lists zero commits (origin/main 8ca9dd7b, 16 commits since base). Accepted.
  • Dev deviation 3 (check:dual-build-cjs-loads not measured locally) — closed by the head: Lint & Repo Gates is success, and the diff emits no JavaScript.
  • Dev deviation 4 (plugin-sharing, service-storage, plugin-audit not run locally) — accepted at the type reading in ①2; Type Check · workspace and Test Core (1–8/8) were still in_progress at read time and are the gate of record for it.
  • Dev deviation 5 (no MCP writes for assignee/reviewer) — not a contract matter; correct under the order.
  • Open question (A: member-level + pin, vs B: whole-interface) — A, for the reason the dev gives and ①4 confirms; B is a narrowing in widening's clothing.
  • Card acceptance — recordIds?: string[]: the PR's ExplainRequest['recordIds'] is pinned equal to string[] | undefined, which is the same member with drift protection. Doc comment: exclusive with recordId, capped by the wire constant (linked), answered by records, implementation may ignore it with the REST per-id fallback named — all four present (L166–181). Clause-②/Metadata-protocol declared on both the PR body and the changeset; check:api-surface agrees (①3). No runtime change: the diff touches no runtime file.
  • Check-runs on 29000bb at 2026-11-02T16:02Z — success: Lint & Repo Gates, Lint · ESLint, Lint · CI/release/engine gates, Lint · repo & tooling gates, Lint · docs & code guards, Lint · PM dispatch-gates, the three scoped lint runs, Type Check · source gates, Check Changeset, Check PR Size, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Dogfood Regression Gate, Build UI Deps, Plan Test Core shards, Auto Label, Flag docs, filter. in_progress: Build Core, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Test Core (1–8/8), Test UI (1–16/16), Type Check UI, Build & E2E UI, Live E2E UI. skipped: the opt-in and docs-only runs. No run is red. The queue's required set is not yet fully concluded; this record judges the contract, and the queue guard reads the runs.

④ Design judgment

The hits file holds against the ruling it implements (card #29297, the follow-up the #29208 landing record owed) and against its first consumers (the REST explain route and the reference SecurityPlugin service). Tried to break it on: optionality (pinned by the {} extends Pick alias, which toEqualTypeOf alone could not hold); input-vs-output side (①4); the readonly mismatch between the reference body's parameter and the contract (string[] is assignable to readonly string[], so an implementer typed either way compiles; the ablation to readonly string[] on the contract side reds the pin, as reported); excess-property behaviour of the plugin's spread wiring (a spread is never excess-checked, so the member reaches the body at runtime — see should-fix 2); ordering and length (①6); the d.ts (①5). None breaks it.

Must-fix: none.

Should-fix:

  1. (hits file, L173) "Mutually exclusive with recordId: a request carrying both is refused" is stated as a property of the contract, while L177–180 lets an implementation ignore recordIds — an implementation that ignores the member cannot refuse the mixed request. Say who refuses: the wire schema at the REST door and the reference implementation; a direct kernel caller must send one spelling, never both. Prose precision only; the semantics declared are the ones both readers enforce.
  2. (outside hits, plugin-security) SecurityPlugin.explainAccessForCaller's own parameter type (security-plugin.ts L2377) still omits recordIds while the extracted body it delegates to reads it (L128) and the service wiring passes it through a spread. Nothing is wrong at runtime or in this PR; it is the same contract-vs-code gap this card closed, one package down. Follow-up card for domain:server, not a rider on this PR.

Implemented-by: claude/project-thread-cb83xj
Reviewed-by: session_01ADHkmQNap7ryQcyQXocWct · fable reviewer for domain:spec#4

VERDICT: PASS

…rdIds

The contract lets an implementation ignore recordIds, so the refusal is not
a property of the contract: the wire schema refuses it at the REST explain
route and the reference security service refuses it. A direct caller of
explain sends one spelling.

Claude-Session: https://claude.ai/code/session_01ADHkmQS1wrCba8FkVowBLy
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct

Contract-review-claim: session_01ADHkmQNap7ryQcyQXocWct · head 57913a3 · 2026-11-02T16:31Z

Delta review of the should-fix commit (record 6581862373, should-fix 1) by the same reviewer.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 57913a379525abcbfb63ab4fba331fda9b2ce136
Local-runs: none
Scope: packages/spec/src/contracts/security-service.ts + packages/rest/src/security-routes.ts, packages/plugins/plugin-security/src/security-plugin-explain-access.ts, packages/spec/src/security/explain.zod.ts (all three byte-identical between 29000bb and 57913a3). The changeset and security-service.test.ts are unchanged on this head and remain the lane seat's ACCEPT at TIER_DEFAULT.

Delta record: one commit on 29000bb (docs(spec): say who refuses a request carrying both recordId and recordIds), +4/-1 in the doc comment of ExplainAccessRequest.recordIds (L174–177). The record on 29000bb stands for everything it judged; this record judges the delta and re-reads the gates on the new head. Net diff against main: the same 3 files, +89/-2.

① Derived judgments

  1. No accept-set or public-surface change — right. The commit edits JSDoc prose only: no member, type, import or export moves. ExplainAccessRequest (interface) stays the api-surface row it was; recordIds?: ExplainRequest['recordIds'] is unchanged, so the type pins in security-service.test.ts hold as before. The emitted d.ts changes only in comment text.
  2. The new sentences are each true of the cited code. "The wire schema refuses a request carrying both at the REST explain route": ExplainRequestSchema superRefine (explain.zod.ts L313–323) is what the route's safeParse runs at security-routes.ts L236, and a failure answers the 400 at L249–253 before any service call. "The reference security service refuses it too": security-plugin-explain-access.ts L142–145 throws on recordIds beside recordId. "An implementation that ignores recordIds does not": true by construction, and it is the exact gap the earlier wording papered over. "A direct caller of explain sends one spelling, never both": the caller-side instruction the contract can actually bind.

② Semver level

Unchanged and still right: @objectstack/spec: patch, Clause-②: yes (widening), Metadata-protocol: none on both the changeset and the PR body. A doc-comment edit moves no level. Check Changeset is green on the head.

③ Boundary flags

  • Should-fix 1 of the 29000bb record — resolved. The sentence no longer promises a refusal of every implementation; it names the two readers that refuse (wire schema at the REST door, reference service) and the one that cannot, and tells a direct caller what to send. This is the wording the earlier record asked for.
  • Should-fix 2 of the 29000bb record (SecurityPlugin.explainAccessForCaller's own parameter type omits recordIds, security-plugin.ts L2377) — unchanged, and correctly so: it was named as a follow-up card for plugin-security, not a rider on this PR.
  • main movement — origin/main 94d3e254 at read time; zero commits on the contract, explain.zod.ts, the REST route or the reference implementation since the branch point 32912139.
  • Check-runs on 57913a3 at 2026-11-02T16:32Z — every non-skipped run completed/success, the queue's required set included: Lint & Repo Gates, TypeScript Type Check (and its four sub-runs: workspace, source gates, consumer gates, debt ledger), Test Core (and 1–8/8), Build Core, Governed Surface Queue Guard, Lint UI (skipped by paths filter), Type Check UI, Test UI (and 1–16/16), Build & E2E UI, Live E2E UI; plus Check Changeset, Check PR Size, PR-body guards, Spec property liveness, Dogfood Regression Gate, Lint · ESLint and the scoped lint runs. skipped: the opt-in and docs-only runs only. No run is red or in progress.

④ Design judgment

The hits file still holds against the card's acceptance and against its two consumers; the delta makes the one doc claim that over-promised precise, and introduces no new claim the cited code does not back. Tried to break it on: whether "at the REST explain route" misattributes the refusal (it does not — the route runs the schema, the schema carries the refine, L236/L313–323); whether the sentence now under-promises for the reference implementation (it does not — L142–145 throws before any evaluation); whether a {@link} or import changed (none did). Nothing new is broken.

Must-fix: none.

Should-fix: none on this PR (should-fix 2 of the prior record remains a plugin-security follow-up card).

Implemented-by: claude/project-thread-cb83xj
Reviewed-by: session_01ADHkmQNap7ryQcyQXocWct · fable reviewer for domain:spec#4

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct

Head-sha: 57913a379525abcbfb63ab4fba331fda9b2ce136 · read 2026-11-02T16:34Z

维护者速读

  • 改了什么:安全服务契约里的 ExplainAccessRequest 类型加了一个可选成员 recordIds(一次解释多条记录的权限),类型直接取自线协议 schema,两边不会再走样。
  • 为什么改:线协议 ExplainRequestSchema 早已接受 recordIds,REST 解释接口和参考实现也都在用;契约类型却没声明,第三方实现看不到批量入口,调用方只能绕过类型。
  • 风险与回滚:只改 TypeScript 类型和注释,运行时零改动;以前能编译的调用照样能编译。回滚就是 revert 本 PR。
  • 先看:packages/spec/src/contracts/security-service.ts 的 ExplainAccessRequest.recordIds 及其注释。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/contracts/security-service.test.ts,预期全部通过(类型钉子确认成员可选、且与 schema 输入类型相等)。
  • 需要你做的:无,席位落地。

席位意见:可落地。契约复审在 29000bb 判 PASS(6581862373),席位要的一句注释修正已在现 head 由同一复审者 delta 复审 PASS(本 PR 上一条记录)。CI 全绿。

席位核对 — ACCEPT · domain:spec#4 · head 57913a37

  1. Shape: draft, base main, Fixes #29297. Clause-②: yes (widening), Metadata-protocol: none and Skills-docs: none — … sit at line start in the body; the changeset .changeset/29297-explain-access-request-record-ids.md (@objectstack/spec patch) carries the same two lines. 3 files, +89/−2: security-service.ts, its test, the changeset, all inside claim 6580454301. No governed path. Author objectstack-fleet[bot] (relay).
  2. Premise measured: on main ExplainAccessRequest declared no recordIds while ExplainRequestSchema did. The member is now recordIds?: ExplainRequest['recordIds'] (ExplainRequest is the schema's z.input); type pins hold it optional and equal to the schema input; the dev's ablation to readonly string[] went red. The docblock was checked against the REST route (safeParse → 400) and the reference service's refusal.
  3. Gates: check-runs on 57913a37: 55 success, 9 skipped (opt-in or path-gated), none failed. Dev ran 79 derived gates on 29000bb1: 78 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, needs a whole-repo build; CI's Build Core is green). check:api-surface records interface names only, so nothing regenerates.
  4. Accepted deviations: only the member is derived, not the whole interface (open question answered A: deriving operation would narrow the deliberate ExplainOperation | string); no merge of main (16 commits, none on the touched files); consumers plugin-sharing, storage and audit left to CI (green); no MCP assignee/reviewer writes (the order forbids them).
  5. Out of scope: SecurityPlugin.explainAccessForCaller's own parameter type omits recordIds → filed plugin-security: SecurityPlugin.explainAccessForCaller types its request without recordIds, though the body it delegates to reads it and the contract now declares it (#29297) #29481 (domain:server, Blocked-by spec(contracts): ExplainAccessRequest does not declare recordIds, which the wire schema and the reference security service both honour #29297). No skills or docs page spells the contract's members (Skills-docs: none).
  6. Landing: not Tier H, under 3000 lines: the seat lands it with pr_ready and auto-merge. After merge the seat writes a landing note on spec(contracts): ExplainAccessRequest does not declare recordIds, which the wire schema and the reference security service both honour #29297 and removes pm:dispatched.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 16:36
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 16:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 5d46f43 Nov 2, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-cb83xj branch November 2, 2026 17:02
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/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant