Skip to content

fix(rest)!: the approvals actions door answers 404 REQUEST_NOT_FOUND for an unknown or invisible request, as its parent door does - #27790

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-kwainq
Oct 30, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #27451
Fixes #27493

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc teaches this door's answer for an unknown or invisible request id; content/docs/api/client-sdk.mdx names the approvals.listActions(requestId) call only

维护者速读

  • 改了什么:GET /api/v1/approvals/requests/:id/actions 遇到不存在、或调用者无权看到的审批单,改为回 404 REQUEST_NOT_FOUND(与父接口 GET …/requests/:id 同一个查找、同一个响应体),不再回 200 空列表。
  • 为什么改:「没有动作记录」与「没有这张审批单」以前无法区分;分诊裁定「404 REQUEST_NOT_FOUND, the same as the parent door.」。
  • 风险与回滚:BREAKING(服务面收窄),只影响对不可读审批单读动作的调用方,SDK 的 approvals.listActions 会改为抛错;可见审批单的答复不变。回滚即 revert 本 PR 一个提交。
  • 先看:packages/rest/src/approvals-routes.ts(readableRequestOr404 与 actions 处理器那一行),packages/rest/src/rest-approvals-actions-unknown-request.test.ts,.changeset/27451-approvals-actions-unknown-request-404.md。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/rest-approvals-actions-unknown-request.test.ts,预期 3 passed。
  • 需要你做的:无,席位落地。

BREAKING (a served-set narrowing of one REST door): reading the action trail of an approval request id that does not exist, or of a request the caller is not allowed to see, now answers 404 REQUEST_NOT_FOUND instead of 200 {"data":[]}. The changeset ships patch with the ADR-0087 disposition not-required (no-migration-prescription): no metadata key changes, only the HTTP answer for a request the caller cannot read.

What changes

request before after
GET /api/v1/approvals/requests/UNKNOWN/actions 200 {"data":[]} 404 {"code":"REQUEST_NOT_FOUND","error":"Approval request 'UNKNOWN' not found"}
GET …/requests/ID/actions, ID exists but the caller cannot see it 200 {"data":[]} the same 404 the parent door gives for ID
GET …/requests/ID/actions, ID visible to the caller 200 {"data":[...]} unchanged

The handler now resolves the parent request before listing. The lookup and the 404 envelope are the parent door's own: they move into one local helper, readableRequestOr404, inside registerApprovalsEndpoints, and both GET …/requests/:id and GET …/requests/:id/actions call it. There is no second copy of the rule. The parent door's answer is byte-for-byte what it was.

IApprovalService.getRequest answers null both for an id it does not hold and for a request outside the caller's visibility (ApprovalService.loadRequest, enforceVisibility: true). So the two cases still answer alike, and the door reveals nothing about which requests exist (the no-leak point of #27493).

Consumers: ObjectStackClient.approvals.listActions throws on any non-2xx answer, so it now rejects for such an id instead of resolving []. In-repo UI callers: RecordApprovalsPanel.tsx already wraps each listApprovalActions(id) in .catch(() => []); approvalRequestsDataSource.ts's timeline read and approvalsApi.ts read the actions of a request they have already resolved. Each reads ids from the request list or the request read, which apply the same visibility rule.

Tests

  • New: packages/rest/src/rest-approvals-actions-unknown-request.test.ts, the real RestServer routes over an approvals stub that models the service's rule (getRequest is null for an unknown id and for a non-participant; listActions is gated by getRequest, as ApprovalService gates it).
    • Unknown id: actions 404, code: REQUEST_NOT_FOUND, body deep-equal to the parent door's body, and listActions is never called.
    • Existing request, caller cannot see it: actions 404, body deep-equal to the parent door's body for the same id. Same status and code as an unknown id, and the message differs only by the id it names. listActions is never called.
    • Visible request: 200 {"data": ROWS} with the rows unchanged, and listActions called once with the id and the caller's context. The parent door still answers 200 with the row.

Acceptance notes

  • The fix adds one getRequest read before listActions, which runs its own getRequest as its tenant gate (approval-service.ts, listActions). That is two reads of the parent per actions request. The service's gate stays as defence in depth for non-REST callers. It is not removed here, because packages/plugins/plugin-approvals is outside this card's file surface.
  • The parent door's handler changed only in shape: its lookup and 404 now come from the shared helper. That was needed to reuse the rule without copying it. Its answer is unchanged. No test drove the parent door's GET before, so the new file pins it too: its 404 (unknown and invisible) and its 200 row (visible).

Verification

Final head acf67294, base 481ade61 (origin/main). The commit after 3e605bac adds assertions to the new test file only.

Pins before the fix (b21c97f, test only): 2 failed, 1 passed
× an unknown request id is 404 REQUEST_NOT_FOUND with the parent door's envelope, and no rows are listed
× an existing request the caller cannot see answers the same 404 as the parent, indistinguishable from an unknown id
AssertionError: expected 200 to be 404 // Object.is equality
Tests  2 failed | 1 passed (3)
Ablation at 3e605ba: parent lookup removed, 2 failed, 1 passed; restored blob == HEAD

Through scripts/ablation-replace.mjs in wrap mode. The anchor if (!(await readableRequestOr404(svc, req.params.id, context, res))) return; went from 1 to 0 on disk, and the marker went from 0 to 1. The subject is imported from src by a relative path, so no dist leg applies.

× an unknown request id is 404 REQUEST_NOT_FOUND ...
× an existing request the caller cannot see answers the same 404 as the parent ...
AssertionError: expected 200 to be 404 // Object.is equality
Tests  2 failed | 1 passed (3)
ablation-replace: ok restored: blob == HEAD (6dc9976135ad) and `git diff HEAD` is empty
  • Changed-package tests: pnpm --filter @objectstack/rest test at 3e605bac: Test Files 343 passed (343), Tests 6695 passed, 326 skipped (7021), VERDICT command-exit 0. The new file again at acf67294: Tests 3 passed (3).
  • Typecheck: pnpm --filter @objectstack/rest typecheck (tsc plus check:test-typecheck, which compiles the test layer under tsconfig.test.json): exit 0.
  • Lint: eslint --no-inline-config on both touched source files: exit 0. The repo-wide pnpm lint is left to CI.
  • Dogfood battery at acf67294 (owed by the (narrowing) arm), under os-verify-lock with CI's command pnpm --filter @objectstack/dogfood test (no shard, no path), after building its closure: VERDICT command-exit 0. Test Files 355 passed, 3 skipped (358). Tests 2867 passed, 20 skipped (2887). No failure.
  • Derived gate set at acf67294 (dispatch-gates --commands, re-derived after the last commit, same 59 families as at 3e605bac): reconciled with --ran, 59 derived, 57 run, 2 NOT-MEASURED, 0 UNRUN. Every run family exited 0, including check-adr-0087-registration (one declared-breaking changeset, not-required (no-migration-prescription)), check-changeset-no-major, check-empty-changeset, check:nul-bytes, check:doc-authoring, check:route-envelope and check:rest-log-spy-declared. The two NOT MEASURED, each exit 3 (PREREQUISITE NOT MET):
    • node scripts/check-plugin-teardown-shape.mjs --self-test: its fixture is pinned to a commit that this shallow checkout cannot reach. CI runs it.
    • pnpm check:dual-build-cjs-loads: it needs every package's dist/. CI's Lint & Repo Gates runs it.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4d1Np3hU7RQnA13YZcL

…for an unreadable parent request

GET /approvals/requests/:id/actions listed rows without resolving the
parent request, so an unknown id, and an existing request the caller
cannot see, answered 200 with an empty list while the parent door
answered 404. Both doors now share one lookup and one 404 envelope.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1Np3hU7RQnA13YZcL
…side the actions door

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx (via REQUEST_NOT_FOUND (literal, a string literal in registerApprovalsEndpoints))
What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 19 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 968cacfd10fdec64b6089d0da4f721f1a8a10925 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from be1ac699f5e556aebfffb9a00d08d3f80a802fa1 — the merge of head acf67294ccc7bf52a92ac5963d4131bcd180f458 into base 968cacfd10fdec64b6089d0da4f721f1a8a10925, 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 be1ac699f5e556aebfffb9a00d08d3f80a802fa1 && git checkout be1ac699f5e556aebfffb9a00d08d3f80a802fa1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 968cacfd10fdec64b6089d0da4f721f1a8a10925 acf67294ccc7bf52a92ac5963d4131bcd180f458 && git checkout -B drift-repro 968cacfd10fdec64b6089d0da4f721f1a8a10925 && git merge --no-ff acf67294ccc7bf52a92ac5963d4131bcd180f458

node scripts/docs-audit/affected-docs.mjs --json 968cacfd10fdec64b6089d0da4f721f1a8a10925

⚠️ 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 968cacfd10fdec64b6089d0da4f721f1a8a10925 → 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:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head acf6729 · 2026-10-29T23:19Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: acf67294ccc7bf52a92ac5963d4131bcd180f458
Local-runs: none
Scope: packages/rest/src/approvals-routes.ts, packages/rest/src/rest-approvals-actions-unknown-request.test.ts + packages/plugins/plugin-approvals/src/approval-service.ts (getRequest, loadRequest, listActions, visibleRequestIds, isOverrideActor), packages/spec/src/contracts/approval-service.ts (IApprovalService.getRequest / listActions), packages/rest/src/rest-server.ts (registerForBase, constructor slots, getRoutes), packages/rest/src/rest-route-ledger.ts, packages/client/src/index.ts (approvals.listActions, fetch), ui/packages/app-shell/src/hooks/useRecordApprovals.ts, ui/packages/app-shell/src/views/RecordApprovalsPanel.tsx, ui/apps/console/src/services/approvalRequestsDataSource.ts, ui/apps/console/src/services/approvalsApi.ts, ui/packages/core/src/adapters/ApiDataSource.ts, packages/qa/dogfood/test/approval-override-composite-pin.dogfood.test.ts, .changeset/27451-approvals-actions-unknown-request-404.md, PR #25628 and its changeset .changeset/25486-unknown-resource-404.md (the #25486 precedent)

Inputs read: cards #27451 and #27493 with every comment (triage 6491511316 and 6495966619), the PR body and file list, the net diff 481ade61..acf67294 (3 files, +174/−5), and the head's check-runs. The seat-posted os-dev report on #27451 was treated as a claim and re-derived from the diff, not adopted.

① Derived judgments

  • The one accept-set change. GET {base}/approvals/requests/:id/actions now calls readableRequestOr404(svc, req.params.id, context, res) before svc.listActions, and returns on a null row. An id the service does not hold, or a request outside the caller's visibility, answers 404 { code: 'REQUEST_NOT_FOUND', error: "Approval request 'ID' not found" } where it answered 200 { data: [] }. That is the triage ruling on approvals: GET /api/v1/approvals/requests/UNKNOWN/actions answers 200 [] while GET /requests/UNKNOWN answers 404 #27451 ("404 REQUEST_NOT_FOUND, the same as the parent door"); approvals: GET /api/v1/approvals/requests/:id/actions answers 200 {"data":[]} for an unknown request id, where GET /api/v1/approvals/requests/:id answers 404 #27493's body said RECORD_NOT_FOUND, and its triage folded it under approvals: GET /api/v1/approvals/requests/UNKNOWN/actions answers 200 [] while GET /requests/UNKNOWN answers 404 #27451's ruling, which the PR follows.
  • Parent door byte-for-byte. Before: const row = await svc.getRequest(req.params.id, context ?? {}); if (!row) { res.status(404).json({ code: 'REQUEST_NOT_FOUND', error: \Approval request '${req.params.id}' not found` }); return; } res.json(row);. After: the helper runs svc.getRequest(id, context ?? {})with the same arguments, writes the sameres.status(404).json({...})literal with the same two keys, same code string and same message template, and the handler doesif (!row) return; res.json(row);. Same status, same code, same message, no header or log line added or removed on the 404 path, same res.json(row)on the 200 path. A throwngetRequeststill lands in the parent's owncatch (500 APPROVAL_REQUEST_GET_FAILED, same logError` prefix). Unchanged.
  • Existence-leak parity holds. ApprovalService.getRequest is loadRequest(id, ctx, true): a row findRequestRow does not return and a row outside visibleRequestIds(...) both exit through return null, one branch for both cases; isOverrideActor callers get null from visibleRequestIds and see every row. So unknown and invisible answer the same status, code and message shape, differing only in the id the caller sent. The new test pins that side by side (unknown vs actions for an outsider: equal status+code, message equal modulo the id).
  • listActions is never reached for an unreadable parent (pinned not.toHaveBeenCalled in both 404 cases). For a readable parent the service's own gate (listActions → getRequest → [] on null) is unchanged, so rows still come back only when the caller can read the parent.
  • Scoped and unscoped variants. Both handlers live in the one registerApprovalsEndpoints body, registered per base path by registerForBase(basePath) and registerForBase(scopedBase); readableRequestOr404 is a closure of that body, so both route variants share the one lookup and envelope. environmentId resolution precedes the lookup exactly as before.
  • Error paths unchanged. respond501 (NOT_IMPLEMENTED) still answers before any lookup in both handlers. A getRequest throw inside the actions handler lands in its existing catch (500 APPROVAL_ACTIONS_FAILED); before the fix the same throw surfaced from listActions' internal getRequest through the same catch, so no new failure code appears.
  • The test drives the real registrar. new RestServer(...) with approvalsServiceProvider in positional slot 12 (after the retired reports slot), registerRoutes(), getRoutes() (rest-server.ts:6361), handler invoked with params: { id }; the stub models the service's rule (getRequest null for unknown and for a non-participant; listActions gated on it). The three pins are red before 3e605bac on the two 404 cases per the PR body, which the diff's shape makes plausible: without the helper call the handler answers 200 { data: [] } for both.
  • Check-runs on acf67294 as read (2026-10-29, ~23:30Z): success — Auto Label, Check Changeset, Check PR Size, Dogfood Regression Gate, Flag docs affected, Governed Surface Queue Guard, Lint · CI/release/engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, PR-body guards, Plan Test Core shards, Test UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, filter. in_progress — Build Core, Test Core 1/8–8/8, Type Check · workspace, Lint · scoped · query-options erasure, Live E2E UI. skipped — the UI build/lint/type/test matrix, Build Docs, Dogfood Verify CLI, Temporal Conformance, the opt-in harnesses. No failure at read time. The landing still waits for the in-progress set to conclude green.

② Semver level

  • .changeset/27451-approvals-actions-unknown-request-404.md: '@objectstack/rest': patch (released package, 18.2.0, not private). Body carries Clause-②: no (narrowing) and Metadata-protocol: none as line-initial declarations, the same two lines as the PR body; the adr-0087: not-required (no-migration-prescription) ... marker in the gate's not-required (category) why shape; a BREAKING banner naming the before and after answer; an "Unchanged" paragraph for a visible request; and a "What changes for you" paragraph telling SDK callers approvals.listActions(id) now rejects. Nothing authorable is removed or renamed, so no FROM → TO migration or tombstone is owed.
  • Consistent with the rest: three doors answer an unknown resource as empty or bad input (publish-drafts 200, flow runs 200 [], datasource PATCH 400) where their siblings answer 404 #25486 precedent: PR fix(rest)!: answer 404 RESOURCE_NOT_FOUND for an unknown resource at the package draft, flow runs and datasource by-name doors #25628's changeset is the same shape line for line — patch, Clause-②: no (narrowing), Metadata-protocol: none, adr-0087: not-required (no-migration-prescription), BREAKING banner on a served-set narrowing, "What changes for you" via ObjectStackClient rejection. AGENTS.md checklist 3 (a non-protocol narrowing ships patch with its BREAKING banner and ADR-0087 disposition) is met. Check Changeset and Lint · CI, release & engine gates (which carries check-adr-0087-registration) are success on the head.

③ Boundary flags

  • Dev flag: two parent reads per actions request. Confirmed: the REST helper runs getRequest, then ApprovalService.listActions runs getRequest again as its tenant gate. Judged efficiency only, as the dev said — both reads answer from the same loadRequest, so no correctness or leak consequence. The service gate is the defence for non-REST callers and lives in packages/plugins/plugin-approvals, outside the dispatch's file surface (approvals-routes.ts handler + tests + changeset); leaving it was right. See ④ should-fix 1 for the follow-up.
  • Fold. The PR body carries Fixes #27451 and Fixes #27493, matching triage 6495966619's duplicate ruling; the Claim on approvals: GET /api/v1/approvals/requests/UNKNOWN/actions answers 200 [] while GET /requests/UNKNOWN answers 404 #27451 names the fold and the branch claude/project-thread-kwainq, which is the PR's head branch.
  • Skills-docs: none — verified: content/docs/api/client-sdk.mdx:368 names client.approvals.listActions(requestId) only; docs/qa/platform-checklist/areas/approvals.json reads /actions only for requests the clause has just created or decided (admin, visible). No doc or skill teaches the old 200 [] for an unknown id.
  • Governed surfaces: none in the file list; Governed Surface Queue Guard is success. The review is owed by the (narrowing) arm alone.

④ Design judgment

The narrowing makes the service's own refusal real at the sub-resource. ApprovalService.listActions already decided "no readable parent → nothing"; the REST door rendered that decision as an empty audit trail, which is the one thing a consumer could misread ("this request exists and has no actions"). After the fix the door renders it with the parent's envelope. Nothing is taken away: the only callers whose answer changes are callers who cannot read the parent, who received no rows before and learn nothing new now (unknown and invisible still answer alike).

Consumers tried, in the cited code:

  • packages/client approvals.listActions — this.fetch throws on !res.ok (index.ts:7684), so it rejects with the not-found message instead of resolving []. Declared in the changeset; the same treatment fix(rest)!: answer 404 RESOURCE_NOT_FOUND for an unknown resource at the package draft, flow runs and datasource by-name doors #25628 gave packages.listCommits and automation.listRuns.
  • Console RecordApprovalsPanel.tsx:285 — listApprovalActions(id).catch(() => []) per id; ids come from useRecordApprovals's request list, the same visibility rule. Unaffected.
  • Console approvalRequestsDataSource.ts findTimeline — ApiDataSource.find throws on non-2xx; it is reached only as the related list of a request record page whose parent read came through findRequest → GET /approvals/requests/:id (same lookup), so a reader who has the page can read the timeline. An admin listing sys_approval_request through the data API is an override actor (visibleRequestIds → null) and sees every request's timeline. Unaffected.
  • Console approvalsApi.listActions — no caller under ui/apps/console/src or ui/packages outside tests. Nothing to break.
  • Dogfood — one reader, approval-override-composite-pin.dogfood.test.ts:211, the admin reading a request it opened: 200. Dogfood Regression Gate is success on the head. No MCP or CLI caller of the door in-repo.
  • Race "visible to listActions but not to getRequest": both are the same getRequest; the only window is a visibility change between the REST read and the service's internal read, which yields 200 { data: [] } from the service gate — the pre-fix answer, no leak, no new failure. The reverse (REST read null, service read visible) cannot list rows because listActions is not called.
  • Route ledger rest-route-ledger.ts:449 carries disposition and client name only, no response-code claim; route metadata.summary unchanged.

Findings:

  1. should-fix (follow-up card in plugin-approvals, outside this PR's surface; not blocking): on the visible path the actions door now runs the full enriched loadRequest twice (REST helper, then the service gate) — servedSubjectFields, enrichRows, attachFlowSteps, attachDecisionProgress each time — and discards the first row. A cheaper visibility probe on the service, or listActions accepting the resolved row, removes the duplicate without weakening the non-REST gate. Efficiency only, as the dev flagged.
  2. should-fix (PR body wording only, no code change): the body lists approvalsApi.ts among UI callers that "read the actions of a request they have already resolved"; approvalsApi.listActions has no caller in the console. The claim is harmless but not what the tree shows.

No must-fix.

Implemented-by: claude/project-thread-kwainq
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

席位意见:ACCEPT。

席位核对

Head-sha: acf67294ccc7bf52a92ac5963d4131bcd180f458

  • Shape: one local helper serves the parent door and the actions door (one copy of the lookup and the 404); new 3-case pin; changeset patch @objectstack/rest with BREAKING banner and ADR-0087 not-required (no-migration-prescription). check-governed-merges --pr 27790: NOT governed, 179 changed lines.
  • Premise measured: red before the fix (2 failed, expected 200 to be 404), ablation of the parent lookup reproduces the same 2 red, restore verified (seat subagent report, card comment on approvals: GET /api/v1/approvals/requests/UNKNOWN/actions answers 200 [] while GET /requests/UNKNOWN answers 404 #27451).
  • Gate check-runs on the head: 48 runs, 35 success, 13 skipped, 0 failure/pending.
  • Contract review: CONTRACT_REVIEW_TIER PASS on this head, record 6511002032 (narrowing face, no must-fix).
  • Accepted deviations: the parent GET /requests/:id handler's lookup moved into the shared helper; the review verified its answer is byte-for-byte unchanged.
  • Review should-fixes, disposition: (1) the visible path reads the request twice (REST helper + the service's own gate) — efficiency only, the service gate stays as defence in depth for non-REST callers; Acceptance notes, not filed. (2) the PR body names ui/apps/console/src/services/approvalsApi.ts as a console caller, but approvalsApi.listActions has no console caller today — body wording only; the relay cannot re-edit a PR body, so it is corrected here.
  • Out of scope: none filed. Skills-docs: not owed (Metadata-protocol: none).
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay. Fixes #27451 and Fixes #27493 close both fold members.

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 tests tooling

Projects

None yet

1 participant