Skip to content

feat(spec): declare IApprovalService.handleActionPage, the optional transport-neutral ADR-0043 action-page member - #22608

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22575-approvals-action-page-member
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22575-approvals-action-page-member

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22575
Clause-②: yes (widening)

What this does

Segment 1 of 4 of the maintainer's ruling A on #22438 (director record 6079645593, the maintainer's 「A + 扫类」). The two parts of that ruling this segment carries, verbatim: "plugin-approvals adds one transport-neutral member that renders on GET and redeems on POST"; "an absent slot or member answers a typed 404 or 501, never ROUTE_NOT_FOUND".

This PR declares the member on the contract. Nothing implements or calls it yet. Segment 2 (the runtime /approvals/act domain and the hono body fix) and segment 4 (the plugin-approvals implementation) are their own cards, each Blocked-by: this one. Segment 3 (the ADR-0069 allow-list) is independent.

  • packages/spec/src/contracts/approval-service.ts: IApprovalService gains one optional member, handleActionPage?(request: Request), which returns a Promise of a web-standard Response (:1148). It sits after the existing optional continueRestoredRun? (:1060). Its docblock states five things:
    • What it serves. GET renders the confirm page from the token query parameter and never decides. POST redeems the token form field through ADR-0043's whole redemption chain. Both answer HTML, and a dead link is told on the page, never as an error status.
    • Authentication. The token is the only credential. The member takes no ExecutionContext and reads no session, cookie or Authorization header. The decision is audited as the approver the token binds.
    • Transport neutrality. There is no Hono, raw-app or http.server type in the signature.
    • Its one caller. That is the runtime HttpDispatcher's /approvals/act domain (segment 2). It forwards the POST body unread, because the token rides in it. The plugin's self-hosted raw-app mount is not a caller, so the path is never mounted twice.
    • The absence rule. If the approvals slot or the member is absent, the caller answers a typed 404 or 501, never ROUTE_NOT_FOUND.
  • packages/spec/src/contracts/approval-service.test.ts: the card's two pins.
    • The type-level pins are three exported aliases. ActionPageIsOptional uses {} extends Pick (toEqualTypeOf cannot tell an optional member from a required T | undefined). ActionPageTakesOneRequest asserts the parameters are exactly one Request. ActionPageAnswersAResponse asserts the return type is a Promise of Response.
    • The value-level pin builds minimalService(), which declares every required member and nothing else. It is typed as IApprovalService and carries no handleActionPage.
    • No docblock prose is pinned, because no consumer parses it.
  • .changeset/22575-spec-approvals-action-page-member.md: @objectstack/spec minor (pre mode next), with its own Clause-②: yes (widening) line.

Why the name handleActionPage

  • The verb handle… is the one this package's contracts already use for a function from a Request to a Response: IAuthService.handleRequest (auth-service.ts:66) and the optional IRealtimeService.handleUpgrade? (realtime-service.ts:127).
  • "Action page" is how the ruling, the card and plugin-approvals' own code (mountActionPages, "Actionable-link pages (ADR-0043)") name ADR-0043's surface.
  • The token verbs (peekActionToken / redeemActionToken) belong to the implementation and stay off the contract. The member is the page, not the token.

The PM's mechanism assumptions, measured

Read at base 99801d831f, re-read at head 63322fcf95 (after merging origin/main 76bc1e03a3).

  1. Confirmed. IApprovalService is at approval-service.ts:845 and continueRestoredRun? is at :1060. The new member follows that optional shape and its docblock style.
  2. Confirmed. Outside packages/spec, git grep IApprovalService names:
    • plugin-approvals: approval-service.ts (class ApprovalService implements IApprovalService, :1280), approval-node.ts (a comment only) and index.ts (a re-export);
    • packages/rest/src/rest-api-plugin.ts (ctx.getService typed to the contract).
    • No hand-built test double typed to the interface exists outside packages/spec. The only typed doubles are minimalService() and its spreads in approval-service.test.ts; sharing-service.test.ts reads Parameters of decide only.
    • objectui at 2a48bd4 has 0 hits (git grep exit 1; the control word IAuthService has 2).
    • No member named handleActionPage existed anywhere before this PR, so no implementer collides with it.
  3. Confirmed, with precedent. packages/spec/tsconfig.json already has lib: ["ES2020", "DOM", "DOM.Iterable"] and types: ["node"], so no new dependency is needed. The published dist/contracts/index.d.ts / .d.mts already carries IAuthService.handleRequest at the same Request → Response shape. A scratch consumer resolved @objectstack/spec/contracts through the package's exports (symlinked, module/moduleResolution NodeNext, skipLibCheck: true). It used two discriminators that compile only if Request / Response fail to resolve inside the published declaration file: a @ts-expect-error on a number assigned to the member's parameter type, and one on a string assigned to its awaited return type.
    • lib: ["ES2020"] + types: ["node"] (no DOM): exit 0, both discriminators consumed.
    • lib: ["ES2020", "DOM"], no types: exit 0.
    • Neither DOM nor @types/node: TS2304 and TS2578. The same TS2578 fires on the identical discriminator over the already-published IAuthService.handleRequest, so this member adds no requirement the entry did not already have.
  4. Read. ADR-0043 and approvals-plugin.ts at head:
    • :411 reads getRawApp();
    • :417 is the GET: it reads c.req.query('token') and answers the confirm or result page;
    • :426 is the POST: it reads token from parseBody() and calls redeemActionToken;
    • every answer is HTML with status 200.
      The docblock describes that behaviour. The plugin is unchanged.

One Zone 3 expectation did not hold. "check:api-surface will see the widened interface" is false. api-surface/contracts.json records each export's name and kind (IApprovalService (interface)), not its members. So check:api-surface answers "public API surface + factory signatures unchanged", check:generated reports all 15 artifacts up to date, and this PR commits no generated file. The widening is visible to the type programs (the reverse verification below), not to a snapshot. The member emits no runtime JavaScript: handleActionPage has 0 hits in packages/spec/dist/**/*.{js,mjs,cjs} and 2 hits in dist/contracts/index.d.ts / .d.mts.

Verification

Heavy runs went through scripts/pm/os-verify-lock.sh. The line VERDICT command-exit 0 is quoted from each.

  • Build. pnpm --filter @objectstack/spec build: exit 0 at d661a20e97. After the merge, turbo run build --filter='@objectstack/plugin-approvals^...' --filter='@objectstack/rest^...' --concurrency=2 rebuilt the consumers' closure, spec included: Tasks: 31 successful, 31 total.
  • check:generated at 63322fcf95. "All 15 generated artifacts are up to date", against the dist that run built.
  • Typecheck at 63322fcf95. pnpm --filter @objectstack/spec --filter @objectstack/plugin-approvals --filter @objectstack/rest typecheck: exit 0.
    • spec runs tsc --noEmit, check:scripts-typecheck and check:test-typecheck. That last one reports "OK", and approval-service.test.ts holds no debt-ledger entry, so the new aliases compile with zero errors.
    • plugin-approvals' check:test-typecheck reports "OK".
    • rest's check:test-typecheck reports "OK".
  • Tests.
    • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/contracts/approval-service.test.ts at 63322fcf95: Tests 8 passed (8) (7 existing + 1 new).
    • The whole spec suite (vitest run --project local --maxWorkers=2) at d661a20e97: Test Files 638 passed (638), Tests 19035 passed | 1 todo. The only commit since is the merge of origin/main, which touches no file of this diff.
    • Consumers' runtime tests were not run. The change is type-only (no emitted JS, above), so typecheck is the consumer measurement. CI's Test Core runs them.
  • Reverse verification, run from the committed d661a20e97 through scripts/ablation-replace.mjs (wrap mode, trap restore):
    • The mutation made the member required: handleActionPage?( became handleActionPage(. On disk the anchor went 1 → 0, the replacement 0 → 1, and the blob c46255afcc33 → ebd1be71170f. After a spec rebuild, ablation-dist-preflight found the marker in dist/contracts/index.d.ts and .d.mts.
    • Expected direction: red. Observed: red.
      • plugin-approvals tsc --noEmit exited 1 with src/approval-service.ts(1280,14): error TS2420: Class 'ApprovalService' incorrectly implements interface 'IApprovalService'. The consumer reads the rebuilt .d.ts.
      • spec's test program reported approval-service.test.ts(175,49): TS2741 Property 'handleActionPage' is missing (the value pin) and (306,43): TS2344 Type 'false' does not satisfy the constraint 'true' (ActionPageIsOptional).
    • Restore leg. The blob is back to c46255afcc33, equal to the HEAD blob, and git diff HEAD is empty. Spec was rebuilt. ablation-dist-preflight --absent reported the required spelling absent from all 232 built files, the optional spelling was present, and the tree was clean. plugin-approvals tsc --noEmit exited 0, and the spec test program reported 0 diagnostics in approval-service.test.ts.
  • Gates at 63322fcf95.
    • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, change set derived from the merge base 76bc1e03a) derived 85 commands. 84 ran and every one exited 0, including check:api-surface, check:docs, check:authorable-surface, check:export-origins, check:exported-any, check:spec-docblock-symbol-anchors, check:adr-0087-registration, check:changeset-no-major, check:empty-changeset, check:doc-authoring, check:nul-bytes, check:issue-citations and check:type-check-coverage / -debt.
    • --ran reconciliation: "85 derived famil(ies) accounted for — 84 run, 1 NOT-MEASURED".
    • All 48 non-PR-context commands of the "Artifact rosters" block exited 0. The three PR-context guards (check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths) are run against this PR once it exists, and their result is reported on the card.

Acceptance notes

  • NOT MEASURED: check:dual-build-cjs-loads. The dispatch rules out a whole-workspace build, which this gate needs. CI runs it.
  • NOT MEASURED in part: check:objectui-pin-citations. It exited 0, but its live objectui round-trip self-test was skipped because the local ../objectui lacks the pinned commit 20c6d351a. That is unrelated to this diff.
  • NOT MEASURED: check:console-injection. It exited 0 but skipped: there is no packages/console/dist in this worktree.
  • Not run locally: the CI type-check lanes (workspace turbo typecheck, ./examples/*, downstream-contract) and the path-scheduled CI jobs (Test Core, Dogfood, Build Core, Temporal Conformance). They are CI's. The consumer typechecks the card names were run (above).
  • check:api-surface does not record interface members (above). That is a fact about the gate's scope, not a defect of this PR, and nothing here changes it.
  • The plugin's action pages answer 200 with an explanatory page for every dead-token shape. The docblock records that as the contract (ADR-0043: "this link is no longer valid", not an error code), so segment 4 implements the member with the plugin's current answers. The typed 404 / 501 of the absence rule is about a missing service or member, never about a dead token.

Generated by Claude Code

…ransport-neutral ADR-0043 action-page member

One optional member on the approvals service contract:
handleActionPage?(request: Request): Promise<Response>. GET renders the
confirm page, POST redeems; the e-mail token is the only credential. Its
one caller is the runtime dispatcher's /approvals/act domain, and an
absent slot or member answers a typed 404 or 501, never ROUTE_NOT_FOUND.

Pins: the member is optional at the web-standard signature, and a
service object without it still satisfies IApprovalService.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, 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/ai/actions-as-tools.mdx (via IApprovalService (symbol, a top-level interface))
What this run could not see
  • the SDK route bridge reached 54 of 206 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 — 139 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 d748ae80afe6ed63d1c61a51a69f4fb18280c844 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json d748ae80afe6ed63d1c61a51a69f4fb18280c844

⚠️ 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 d748ae80afe6ed63d1c61a51a69f4fb18280c844 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 63322fcf95afb83afb293192a98d69cd928eb374
Local-runs: none

Scope read: PR #22608 for card #22575 (segment 1 of ruling A on #22438, director record 6079645593, the maintainer's 「A + 扫类」; the split request 6092059305). The branch claude/issue-22575-approvals-action-page-member resolved to the head above, which begins 63322fcf95 as the brief expected: it is the merge of d661a20e97 with origin/main 76bc1e03a3, and the net diff against that merge-base is 3 files, +105 / −0 (packages/spec/src/contracts/approval-service.ts +56, packages/spec/src/contracts/approval-service.test.ts +36, .changeset/22575-spec-approvals-action-page-member.md +13). The API file list and the git diff agree. No governed path; the head repository is the base repository; size/m. Inputs were the card and its three comments, the two authority comments on #22438, the PR body, file list and net diff, the head's check-runs, and ADR-0043 and ADR-0138 on main; the plugin's code on main was read where the docblock makes claims about it.

① Derived judgments

  1. The member's signature — right. IApprovalService gains exactly one optional member, handleActionPage?(request: Request), returning a Promise of a web-standard Response, placed after the existing optional continueRestoredRun?. One Request parameter and nothing beside it: no ExecutionContext, no Hono context, no raw app, no http.server type. That is the ruling's item 1 as it binds this segment ("packages/spec declares the member, optional, on the approvals service contract") and the split request's segment 1 (transport-neutral, renders on GET, redeems on POST), and token-only authentication is what a signature with no context can carry. The shape is the contracts' own precedent on main: IAuthService.handleRequest(request: Request) (auth-service.ts:66) and the optional IRealtimeService.handleUpgrade? (realtime-service.ts:127) answer a Promise of Response from one Request, so the published declaration file already carries Request / Response and this member adds no new consumer requirement (packages/spec/tsconfig.json lists DOM in lib and node in types; the dev's resolution probe agrees).

  2. Every existing implementer and double still satisfies the contract — right. On main the only implements IApprovalService is ApprovalService in plugin-approvals (approval-service.ts:1327); the typed doubles are minimalService() and its two spreads in approval-service.test.ts; sharing-service.test.ts reads only the parameters of decide; rest-api-plugin.ts consumes the slot through getService typed to the contract; no Required, Pick, Omit, Partial or keyof over the interface exists anywhere; no member named handleActionPage existed before (0 hits). An optional member obligates none of them. The mechanical form of this judgment is on the head: TypeScript Type Check and its four lanes (workspace, source gates, consumer gates, debt ledger) are success. The dev's reverse check (the required spelling turns plugin-approvals red with TS2420 and the pins red with TS2741 / TS2344, restore blob-equal) shows the member is the only thing that moved.

  3. The docblock against plugin-approvals on main — right, with one nuance stated. Read against approvals-plugin.ts:392–437 (mountActionPages), approval-service.ts:4812–4868 (resolveActionToken, peekActionToken, redeemActionToken) and action-link-pages.ts:63–78 (renderConfirmPage):

    • GET reads token from the query string, calls peekActionToken (its own doc: validate WITHOUT consuming, never mutates), and answers the result page on a dead token or the confirm page, which is the request summary plus a form with method post to the act path carrying a hidden token input. The docblock's "renders, never decides, a form that POSTs the token back": match.
    • POST reads token from the parsed form body and calls redeemActionToken: SHA-256 hash lookup, not consumed, not expired, request still pending, approver_id still in pending_approvers, then consumed_at written, then decide with actorId = the token's approver. The docblock lists exactly that chain in that order: match.
    • The dead-token reasons on main are invalid, consumed, expired, not_pending, not_approver; the docblock's "invalid, expired, consumed or no longer live" covers them: match.
    • Every answer is text/html with status 200, dead tokens included; ADR-0043's Consequences say "this link is no longer valid", not an error code. The docblock's "a dead link is told on the page, never as an error status": match.
    • The handlers read nothing but the token (no cookie, session or Authorization header): the docblock's "the token is the only credential": match.
    • "Audited as the approver the token binds, never as a system actor": redeemActionToken decides with actorId = the token's approver and a context whose userId is the person behind the slot (bare SYSTEM_CTX only when no single account matches an email-bound slot, in which case the action row's actor_id records no person and acted_as records the slot). The audit row never carries a system sentinel. The nuance: the execution context under the decision IS system-elevated (the code's own words: isSystem still stands in for the missing session; ADR-0043 Mechanics: the system context carries the bound identity). The docblock speaks of audit attribution, which is what the code does; segment 4 should read "never as a system actor" as a statement about the audit row, not about the context's elevation.
    • "Its one caller", the forwarded-unread POST body and "never mounted twice" are not claims about today: nothing calls the member yet (the PR and the changeset say so). They restate the ruling's item 1 (the dispatcher domain forwards to the slot; the plugin keeps its raw-app mount byte-unchanged; the hono catch-all stops consuming non-JSON bodies) and bind segments 2 and 4, not this diff. Consistent with the authority.
    • The absence rule is the ruling's acceptance 3 verbatim (a typed 404 or 501, never ROUTE_NOT_FOUND): right.
    • ADR-0043's id is left in the code, as Prime Directive [WIP] Add Chinese version of the documentation #13 asks; no scripts/adr-anchors/ file is owed for a contract declaration. Right.
    • Unstated, and not a gap of this segment: what the member answers to a method other than GET or POST. The ruling defines the caller as an exact domain for GET and POST, so the question belongs to segment 2's domain. Non-blocking.
  4. Public surface and generated artifacts — right. The member emits no JavaScript. packages/spec/api-surface/contracts.json records IApprovalService (interface) by name and kind only, so no generated artifact moves; check:generated reporting every artifact current is the correct answer, not a miss, and check:api-surface's "unchanged" is correct within its declared scope. Accept set: no Zod schema, no authorable key, no stored shape and no REST route is touched, so there is no accept-set change at all; the widening is TypeScript-only.

  5. The pins — right. ActionPageIsOptional uses the empty-object-extends-Pick form, the one form that tells an optional member from a required T | undefined (the file's own precedent for continueRestoredRun); ActionPageTakesOneRequest and ActionPageAnswersAResponse use the file's strict Eq identity over Parameters and ReturnType; the value pin types minimalService() (every required member and nothing else) as IApprovalService and asserts the member is undefined. The aliases are exported for the TS6196 reason the file already documents, and Type Check · debt ledger is success with no debt entry for the file. The dev's ablation shows each pin fails in the direction it claims.

  6. The name — right. camelCase, the contracts' handle verb for a Request-to-Response member, and the ADR-0043 / plugin noun (mountActionPages, "actionable-link pages").

② Semver level

  • The diff publishes in one package, @objectstack/spec (the test file ships nowhere; the changeset is not a surface). Its changeset 22575-spec-approvals-action-page-member.md declares "@objectstack/spec": minor and carries Clause-②: yes (widening) on its own line, with a body that states the member, the token-only credential, transport neutrality, the absence rule and that nothing implements or calls it yet. Pre mode is next (.changeset/pre.json). Under the Post-Task Checklist, yes takes at least minor and (widening) is the non-breaking arm; nothing is removed, renamed or narrowed, so no ADR-0087 marker is owed. Check Changeset is success on the head. Not skip-changeset, correctly: the package publishes the widened declaration file. Right.
  • The PR body's second line reads Clause-②: yes (widening), identical to the changeset's line and to the dispatch claim's. Right. The body's description of the member, the pins and the changeset matches the diff.
  • .changeset/config.json holds @objectstack/spec in the fixed group, so every listed package bumps together; the declared level is the publishing package's own, as it should be.

③ Boundary flags

  • Dev deviations (4), answered. (a) One merge of origin/main before the PR opened: the net diff against the merge-base is the three files; no finding. (b) Model-free commit trailers and the session-URL PR footer: the form AGENTS.md prescribes; no finding. (c) Consumer runtime suites not run locally: the member emits no JavaScript, so typecheck is the consumer measurement, and CI's Test Core is the runtime one (still in_progress at this read, below). (d) Worktree removed: housekeeping; no finding.
  • Acceptance notes, answered. check:dual-build-cjs-loads NOT MEASURED locally (needs a whole-workspace build; it runs under Lint & Repo Gates, which is success on the head). check:objectui-pin-citations partial and check:console-injection skipped: environment facts unrelated to this diff. The 200-for-every-dead-token note: verified against the plugin in ①.3 and consistent with ADR-0043; it binds segment 4 to the plugin's current answers, which is the ruling's intent.
  • open_questions: none. out_of_scope_findings (1), answered. check:api-surface records export names and kinds, not interface members, so a member-level widening is invisible to it. Correctly noted and not filed: AGENTS.md already states that the api-surface snapshot records that an export exists and never what it resolves to (the reason check:exported-any exists), so this is a known scope boundary rather than a wrong answer against the gate's contract, and the type programs are the instrument that saw this widening. No card.
  • ADR-0043 (Accepted): no conflict. The member is ADR-0043's own surface restated transport-neutrally: token-only, session-less, GET renders and never executes, POST redeems through the ADR's table, dead links told on the page. The one phrase to keep straight is the context, judged in ①.3: the ADR's "system context carries the bound identity" and the docblock's "audited as the approver" are compatible.
  • ADR-0138 D2 (Proposed): no conflict on this segment; the classification the ACCEPT carries to design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146 is what is owed. D2 closes the list of door classes that serve an unauthenticated request (six classes, everything else 401 per domain, and "naming a new door class is an amendment of this record"), and names what sits outside the guest model by construction: the ADR-0069 control-plane allowlist, the signed inbound-hook channel whose per-flow secret is the credential, and MCP. The approvals action page is a token-addressed door that exists today on the self-hosted raw-app mount under ADR-0043, and ADR-0138 neither lists it, amends ADR-0043, nor names ADR-0043 among the records it leaves unchanged: it is silent on it. This diff does not open, move or widen that door; it declares a contract member nothing calls. So nothing here contradicts D2, and ADR-0138's own "no change this record decides lands in packages/** before acceptance" does not reach a change decided by ADR-0043 and ruling A. What is owed is the reading the ACCEPT already placed on design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146: the next G2 reading classifies the act page, either as a capability-token door beside class 2 (share links) or as outside the guest model beside the signed inbound-hook channel (the token is the credential; no principal, no guest envelope, no shouldDenyAnonymous call), and that reading should land before or with segments 2 and 3, which are the changes that make the door reachable on a dispatcher-only kernel through the ADR-0069 allow-list. Escalation beyond that pointer: none.
  • Governance and landing shape. No governed surface in the file list; Governed Surface Queue Guard is success; 105 changed lines; not a fork. The contract review is owed because the dispatch marked a published-contract widening; this record is it. The PR stays a draft marked needs:contract-review; the owning seat readies and arms it only once every required check is green.
  • CI on the head, read at 2026-10-10T04:53Z. Of the seven required contexts, Lint & Repo Gates, TypeScript Type Check, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard are success; Test Core (its six shards; the aggregate run appears after them) is still in_progress. No run on the head has failed; every other completed run is success or skipped by its own path filter. GitHub reports the PR mergeable against main (mergeable: true; mergeable_state: blocked, the state of a draft with checks still running). This verdict is the contract reading and does not stand in for that context: landing waits for it.

Implemented-by: claude/issue-22575-approvals-action-page-member
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing pre-checks at 63322fcf95, by the owning seat

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-10T05:04Z · holder of claim 6093365428.

  • The review: contract review PASS 6093956534 names this head. It confirms the member against ruling 6079645593 and the split 6092059305, every implementer and test double, and the docblock against plugin-approvals on main. It finds no conflict with ADR-0043, and on ADR-0138 D2 only the classification already carried to design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146 (6092511536). Its two notes for segment 4 are recorded there: the audit row versus the elevated execution context, and methods other than GET / POST.
  • CI: 33 success, 6 skipped, with every required context green. check-expected-skips --pr 22608 reads "OK — 6 skipped check-run(s), every one in the roster", and no other conclusion is on the head.
  • Governed: check-governed-merges --pr objectstack-ai/objectstack#22608 reads NOT governed; 105 changed lines.
  • Closing keywords: the body carries Fixes #22575 alone, and no commit message carries one.
  • main drift since the merge base 76bc1e03a3: main moved none of the PR's 3 paths. GitHub reports mergeable: true, mergeable_state: clean.

needs:contract-review comes off; pr_ready and automerge_enable follow. When it lands, segments 2 and 4 of #22438, which are Blocked-by: this card, unlock through the triage scan.


Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438)

2 participants