Skip to content

feat(spec): a strict response contract for GET /auth/me/permissions, and hasAnyQueue on IOwnershipQueueService - #28704

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-7xha1v
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-7xha1v

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Requested by Leehom · project thread

Fixes #28508

Clause-②: yes (widening)
Metadata-protocol: added

维护者速读

  • 改了什么:GET /auth/me/permissions 第一次有了 spec 里的严格响应契约 MePermissionsResponseSchema(@objectstack/spec/api),覆盖匿名、降级(无 SecurityPlugin)、完整、故障关闭四种响应体,声明了 hasAnyQueue: boolean,未声明的同级键直接拒绝。IOwnershipQueueService 契约加上可选方法 hasAnyQueue?(organizationId),路由改为通过契约读取,删掉本地的 AnyQueueReader 可选类型。控制台 @object-ui/permissions 的 MePermissionsResponse 改为从 spec 派生,不再手写。
  • 为什么改:Session payload: the console learns once per session whether the caller's organization has at least one ownership queue (declared or sys_queue), with no widening of member read on sys_queue #28508 的 spec 一半(服务端一半已由 feat(plugin-hono-server,plugin-sharing): /auth/me/permissions answers hasAnyQueue from the ownership-queue service #28654 合入)。控制台每个会话读一次这个响应来驱动整个权限层,之前只有 ui 里手写的接口,服务端多发或少发一个键都没人拦。手写接口已经漂移:它读一个服务端从不发送的 roles 键(服务端的键是 positions),所以 MePermissionsProvider 下的 usePermissions().roles 一直是空数组。
  • 风险与回滚:线上响应字节不变。hasAnyQueue? 是可选方法(与 declaredQueueNamed? 一样),仓库外的旧实现照样满足契约,路由对缺这个方法的实现答 false。控制台行为只有一处变化:usePermissions().roles 从恒为空变为服务端的 positions(仓库内没有生产代码读它)。@object-ui/permissions 导出的 MePermissionsResponse 变为 spec 的联合类型,按它写的测试夹具随之迁移(roles → positions,加 hasAnyQueue)。回滚即 revert 本 PR。
  • 先看:packages/spec/src/api/auth.zod.ts(三个 schema)、packages/spec/src/contracts/ownership-queue-service.ts(新方法)、packages/spec/src/api/me-permissions-response.test.ts(契约测试 12 例)、ui/packages/permissions/src/MePermissionsProvider.tsx(派生类型与 roles)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/me-permissions-response.test.ts,预期 12 passed;pnpm --filter @objectstack/plugin-hono-server exec vitest run src/current-user-endpoints-has-any-queue.test.ts,预期 9 passed(每个真实响应体都过严格 schema)。
  • 需要你做的:无。contract review owed:入队前由席位按 CONTRACT_REVIEW_TIER 给出 ## Contract review PASS(567e3dc8 上的首轮为 FAIL,本版逐条修复,待增量复审)。

What changes

  • MePermissionsResponseSchema (packages/spec/src/api/auth.zod.ts), a z.discriminatedUnion('authenticated', …) of two strict branches:
    • MePermissionsAnonymousResponseSchema: { authenticated: false } and nothing else, so nothing about a user can ride on the anonymous body.
    • MePermissionsAuthenticatedResponseSchema: userId, tenantId (nullable), positions, permissionSets, objects (record of EffectiveObjectPermissionSchema), fields (record of a strict { readable, editable }), systemPermissions and tabPermissions (both optional: absent only on the degraded body, where absent means unknown, and present-and-empty means none), and hasAnyQueue: boolean (required on every authenticated body).
    • Types MePermissionsResponse, MePermissionsAnonymousResponse, MePermissionsAuthenticatedResponse (z.input). The three schemas are isomorphic, so they take ADR-0122 Iso_ pins in type-alias-convention.pin.test.ts (count 808 → 811) rather than Parsed aliases.
  • IOwnershipQueueService.hasAnyQueue?(organizationId: string | null): Promise<boolean> (packages/spec/src/contracts/ownership-queue-service.ts): true exactly when some name would resolve through resolveQueue. Optional, like declaredQueueNamed?, so an implementation written before it still satisfies the contract; the route answers false without it. Its docblock names /auth/me/permissions as the one carrier and says it discloses no name and no count. plugin-sharing's OwnershipQueueService already implements it (feat(plugin-hono-server,plugin-sharing): /auth/me/permissions answers hasAnyQueue from the ownership-queue service #28654); only its docblock changes.
  • @objectstack/plugin-hono-server (current-user-endpoints.ts): the route reads the method as Pick<IOwnershipQueueService, 'hasAnyQueue'> through the kernel locator, keeping the runtime typeof guard so an older implementation still answers false. mePermissionsBody (the full and closed bodies) returns MePermissionsAuthenticatedResponse, so tsc refuses a key the schema does not declare. The degraded literal and the anonymous { authenticated: false } stay bare c.json({...}) literals: an inline satisfies hid both from the check:route-envelope census (found 7, pinned 9), so they are bound by the strict parse in the route and door tests instead. The wire is byte-for-byte unchanged.
  • @object-ui/permissions (ui/packages/permissions/src/MePermissionsProvider.tsx): MePermissionsResponse is re-exported from @objectstack/spec/api (with MePermissionsAuthenticatedResponse beside it) in place of the hand-written interface check:spec-symbols flagged. The provider reads grants only from the authenticated branch. The interface's roles key was drift (the server sends positions), so the context's roles now carries positions. ui/apps/console/src/components/studioEntry.ts narrows on authenticated before reading systemPermissions. Test fixtures across 13 UI packages typed as the response move roles: [...] → positions: [...] and add hasAnyQueue: false; builders that answer authenticated: true are typed as the authenticated branch; three { readable: false } field entries gain editable: false (the strict entry declares both); the anonymous fixtures are { authenticated: false }.
  • Skill index. An objectstack-api row in TRANSITIVE_ALLOWLIST (packages/spec/scripts/lib/skill-map-guards.ts) lists the 13 transitive pointers the index already published, so the new auth.zod.ts → security/permission.zod.ts edge no longer drags 22 more into skills/objectstack-api/references/_index.md (the file is now identical to main's).
  • Variant-doc ledger. The authenticated:false|true union is declared not-authorable in packages/spec/variant-docs.json.
  • Generated artifacts regenerated with pnpm --filter @objectstack/spec check:generated --fix: api-surface, export-origins, declaration-map, authorable-surface and json-schema manifests, the reference docs (content/docs/references/api/auth.mdx, index.mdx 1582 → 1585) and the strictness ledger counts (472 → 475).
  • Changeset .changeset/28508-me-permissions-response-contract.md: minor for @objectstack/spec (Metadata-protocol added: the authorable-surface baseline moves, per the precedent of feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape #28440 / feat(spec): declare the publish-drafts request body, so the change note is part of the published API contract #28447 / feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595), patch for @objectstack/plugin-hono-server, @objectstack/plugin-sharing and @object-ui/permissions, with the roles → positions mapping for a fixture author.

Tests

  • Contract (packages/spec/src/api/me-permissions-response.test.ts, new, 12 cases): the anonymous, full, closed and degraded bodies parse; hasAnyQueue is declared, must be a boolean ('yes', 2 refused at path hasAnyQueue) and is required; an unknown sibling on the authenticated body is refused with unrecognized_keys naming the key, not dropped; an unknown sibling on the anonymous body is refused too (hasAnyQueue, userId); a field entry is closed; absent and empty systemPermissions both parse and stay distinct; a tab visibility outside the four is refused at tabPermissions.sales; a non-boolean discriminator matches no branch.
  • Route (plugin-hono-server/src/current-user-endpoints-has-any-queue.test.ts): every body the handler answers in the 9 cases (full, degraded, closed, anonymous, service absent / without the method / throwing) is now also parsed through MePermissionsResponseSchema and must have no issues.
  • HTTP door (verify/src/me-permissions-has-any-queue.test.ts, 5 cases): the raw body each real boot answers is parsed through MePermissionsResponseSchema and must have no issues, before hasAnyQueue is read.
Red first, green after, ablations
  • Red on main. The contract test against origin/main's auth.zod.ts: every case red (the three schemas do not exist).
  • Green. Contract 12/12, route 9/9, door 5/5.
  • Ablation A, strictness dropped (.strict() → .passthrough() on the authenticated branch): the contract case "an unknown sibling is refused on the authenticated body" goes red, 1 failed / 11 passed. Restored: blob == HEAD.
  • Ablation C, the route sends an undeclared key (ablation-replace.mjs adds ablation28508ExtraKey to mePermissionsBody; ablation-dist-preflight finds the marker in plugin-hono-server dist/index.js and dist/index.mjs): route test 7 failed / 2 passed (the degraded and anonymous cases do not go through mePermissionsBody, by design), door test 5 failed (5). Restored: blob == HEAD, rebuilt, marker absent. This is the reading that the route and door tests are bound to the strict schema.
  • Ablation B, not measured. Renaming hasAnyQueue in the spec schema was refused by the spec build itself (the gen:schema baseline gate refuses a key removal), so dist stayed at HEAD and no test reading dist could see it. Ablation A covers the contract side and C the consumer side.

Acceptance notes

Verification

All readings on head 0eb64410 (the final commit), on main 3944d2e1. The first round (head 567e3dc8, whole-workspace build 118/118) is in the PR history; this round changes the spec contract, the changeset and ui/, so everything below was re-measured after rebuilding the spec, plugin-sharing, plugin-hono-server and @object-ui/permissions.

  • Contract review fixes. Must-fix 1: changeset and body declare Clause-②: yes (widening) / Metadata-protocol: added, @objectstack/spec minor; node scripts/check-changeset-no-major.mjs --base origin/main with GITHUB_EVENT_PATH set to a pull-request event carrying this body: exit 0. Red first: the same command with 567e3dc8's changeset and the old none body exits 1 with CI's refusal ("metadata-protocol declares none while an authorable-surface or authorable-defaults baseline moved · Moved: packages/spec/authorable-surface/api.json"). Must-fix 2: hasAnyQueue? optional. Should-fix (a): skills/objectstack-api/references/_index.md is identical to main's (git diff origin/main empty). Should-fix (b): the doc cross-reference is in auth.zod.ts.
  • UI. pnpm --dir ui run check:spec-symbols: exit 0 ("0 untriaged collisions"). Typecheck (tsc --noEmit and tsc -p tsconfig.test.json) of @object-ui/permissions, plugin-kanban, plugin-calendar, plugin-detail, plugin-gantt, plugin-form, plugin-view, plugin-list, plugin-grid, fields, plugin-report and app-shell, plus apps/console tsc --noEmit: 0 errors each. Tests: @object-ui/permissions 15 files, 166/166; every UI test file that names the response type, the provider or the route (143 files across those packages): Test Files 143 passed (143), Tests 1761 passed (1761). eslint on the provider, the package entry and studioEntry.ts: exit 0.
  • Package suites. @objectstack/spec: 23185 passed, 1 todo. @objectstack/plugin-hono-server: 466/466. @objectstack/plugin-sharing: 1302/1302. Door test: 5/5. VERDICT command-exit 0.
  • Typecheck. pnpm --filter @objectstack/spec --filter @objectstack/plugin-sharing --filter @objectstack/plugin-hono-server --filter @objectstack/verify typecheck: exit 0.
  • Generated artifacts. pnpm --filter @objectstack/spec check:generated: exit 0 after --fix regenerated gen:skill-refs for the allowlist row.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 115 commands, all 115 exit 0 on 0eb64410; --ran: "115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED".
  • Not local. test:repo, the full Test Core shards and the full UI suite are CI's. No full dogfood battery is owed: no ! in the title and no (narrowing) arm.
Gate record (115 lines, COMMAND :: exit N)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:generated :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:authz-resolver :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-stale-fill :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Generated by Claude Code

…and hasAnyQueue on IOwnershipQueueService

Co-Authored-By: Claude <noreply@anthropic.com>
…its declared contract

Co-Authored-By: Claude <noreply@anthropic.com>
…se contract; changeset

Co-Authored-By: Claude <noreply@anthropic.com>
… in no route ledger the client conformance test reads

The client's URL conformance test (client-url-conformance.test.ts) requires every
SDK URL to match an exact ledger route; /auth/me/permissions is mounted by
plugin-hono-server and appears in none, so the method would be credited only by
the * /auth/** wildcard. The typed read is left to the consumer card.

Co-Authored-By: Claude <noreply@anthropic.com>
…ions response schemas; changeset drops the client

Co-Authored-By: Claude <noreply@anthropic.com>
…ons response contract on main 3944d2e

Co-Authored-By: Claude <noreply@anthropic.com>
…the variant-doc ledger (not-authorable)

Co-Authored-By: Claude <noreply@anthropic.com>
…missions bodies as bare literals the route-envelope census counts

The inline `satisfies` hid both from check:route-envelope (found 7, pinned 9). The strict schema parse in the route and door tests still binds them.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-hono-server, @objectstack/plugin-sharing, @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/permissions/permission-metadata.mdx (via default_off (literal, a string literal in MePermissionsAuthenticatedResponseSchema), default_on (literal, a string literal in MePermissionsAuthenticatedResponseSchema))
  • content/docs/permissions/permission-sets.mdx (via default_on (literal, a string literal in MePermissionsAuthenticatedResponseSchema))
What this run could not see
  • 6 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 170 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 db357f6da9f787ff6285c20c70bc02a1bc138d35 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 db357f6da9f787ff6285c20c70bc02a1bc138d35 → 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#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 567e3dc8230bd5af5d160a005efee9cecb831964
Local-runs: none
Scope: packages/spec/src/api/auth.zod.ts, packages/spec/src/contracts/ownership-queue-service.ts, skills/objectstack-api/references/_index.md + packages/plugins/plugin-hono-server/src/current-user-endpoints.ts, packages/plugins/plugin-hono-server/src/current-user-endpoints-has-any-queue.test.ts, packages/plugins/plugin-sharing/src/ownership-queue-service.ts, packages/spec/src/security/permission.zod.ts, packages/spec/src/api/protocol.zod.ts, packages/spec/scripts/build-skill-references.ts, packages/spec/scripts/lib/skill-map-guards.ts, packages/spec/authorable-surface/api.json, .changeset/28508-me-permissions-response-contract.md, packages/verify/src/me-permissions-has-any-queue.test.ts, and the ui reader ui/packages/permissions/src/MePermissionsProvider.tsx (objectui), read against card #28508 (ruling record 6557794880, server-half landing 3944d2e, os-dev-report 6559267240) and consumer card #28428.

Check-runs on the head at review time (42): Check Changeset completed failure (step "Guard against accidental major bumps (launch window)", verdict none-baseline-moved); Governed Surface Queue Guard, PR-body guards, Check PR Size, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Spec property liveness, Plan Test Core shards, Dogfood Regression Gate, Flag docs affected, Check Documentation Links, Auto Label, filter completed success; Test Core (1..8/8), Build Core, Build Docs, Build UI Deps, Live E2E UI, every Type Check · and every other Lint · job still in_progress; the rest skipped. So the derived gate families are not yet answered by CI; the dev's 115/115 local gate record is the dev's account, not a gate verdict, and the one completed gate that reads the declarations is red.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named:

  1. @objectstack/spec/api gains six exports — MePermissionsAnonymousResponseSchema, MePermissionsAuthenticatedResponseSchema, MePermissionsResponseSchema and their three z.input types (api/index.ts already export *s auth.zod). Right: api-surface, export-origins, declaration-map (6 each) and json-schema manifest (3 schema ids) all carry exactly these and nothing else; content/docs/references/api/auth.mdx and index.mdx (1584 to 1587, API 460 to 463) are the generator's output of the same.
  2. The schema matches every body the live route answers. Read in current-user-endpoints.ts on the head: anonymous { authenticated: false } (bare literal, line ~900) is exactly the strict anonymous branch; the degraded literal (no resolver or no metadata, ~line 940) sends authenticated, userId, tenantId (?? null), positions, permissionSets, objects: {}, fields: {}, hasAnyQueue with systemPermissions/tabPermissions absent — the two optional keys, every other key required: matches; the full body and the outer-catch closed body both go through mePermissionsBody, now return-typed MePermissionsAuthenticatedResponse, with all eleven keys. positions from execCtx.positions (z.array(z.string()).default([])), permissionSets from execCtx.permissions (same) or resolved.map(name).filter(Boolean), fields from buildEffectiveFieldPermissions whose return type is exactly Record of { readable: boolean; editable: boolean } (strict entry holds), tabPermissions merged only from the four-value tabRank vocabulary (the enum holds), objects from buildEffectiveObjectPermissions whose entries are EffectiveObjectPermissionSchema. Right. The schema is bound, not declaration-only: tsc pins the two mePermissionsBody bodies to the type; the route test's read() and the verify door test parse every provoked body (full, degraded, closed, anonymous, service absent / method absent / throwing) through MePermissionsResponseSchema with issues == []. The degraded and anonymous literals are bound only by those tests (the dev's check:route-envelope reason for keeping them bare is read in the file's own comment and is accepted).
  3. objects entries stay tolerant. EffectiveObjectPermissionSchema is .strip() by design (wire-side tolerance, 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001) wrapped in acceptRetiredDefaultResidue. So "an unknown sibling is refused" is true at the top level and in a fields entry, and NOT inside an objects entry (dropped silently). The dev disclosed it; the card's Acceptance 1 names the sibling of hasAnyQueue, which is top-level. Right as scoped, recorded as a limit of the contract, not a defect.
  4. IOwnershipQueueService gains a REQUIRED member hasAnyQueue(organizationId). This is a public-surface change to a published contracts/ interface that out-of-tree code implements (the module's own header: "a host registered under the name"). It is a widening for callers and a compile-time break for implementers. Judged wrong as declared — see ② and ④.
  5. packages/spec/authorable-surface/api.json gains 11 keys (api/MePermissions*Response:*). Right as generated output; but a moved authorable-surface shard IS the baseline check-changeset-no-major reads for the level axis, and the PR declares Metadata-protocol: none. See ②.
  6. Strictness ledger 474 to 477: three new strict sites (anonymous object, authenticated object, the fields entry). Right.
  7. variant-docs.json gains authenticated:false|true as not-authorable. Right: a wire discriminator, nobody authors it.
  8. ADR-0122 pins 808 to 811 as Iso_ (not Parsed): right only if the three schemas are isomorphic; the pin is tsc-checked, and the authenticated branch nests EffectiveObjectPermissionSchema (a pipe over a strict-extended-then-stripped object). The dev's typecheck is the only reading; CI Type Check is in progress. Accepted provisionally on the pin's own mechanism.
  9. @objectstack/plugin-hono-server: AnyQueueReader becomes a Pick of the contract's hasAnyQueue, read as Partial through the locator with the typeof guard kept, and mePermissionsBody is return-typed. Wire byte-identical (no key added, removed or re-ordered). Right.
  10. @objectstack/plugin-sharing: docblock only. Right; OwnershipQueueService.hasAnyQueue already landed in 3944d2e (line 222 on main) and matches the contract's docblock claim ("true exactly when some name would resolve through resolveQueue": active declared queues, else authored names not under a declared name).
  11. skills/objectstack-api/references/_index.md: 22 new "Transitive dependencies" pointers and no new core entry. Right as generated: the new import { EffectiveObjectPermissionSchema } from '../security/permission.zod' in a core file of the objectstack-api skill pulls security/permission.zod.ts and its closure (data/field, data/object, ui/view, ui/chart, ui/bulk-action, automation …) into the index. Accurate, but see ④.
  12. Changeset: patch × 3 with Clause-②: yes and Metadata-protocol: none. Wrong — see ②.

② Semver level

Must-fix (gate-red). Check Changeset on this head FAILS with none-baseline-moved: the PR declares Clause-②: yes with no arm and Metadata-protocol: none, and moves packages/spec/authorable-surface/api.json. The workflow's own rule (pr-automation.yml "WHICH LEVEL", maintainer ruling A 2026-10-21): a declared none "may not move a packages/spec/authorable-surface/ shard (none-baseline-moved, exit 1)", and "a baseline is never edited to quiet this gate". The PR body's reading ("the manifest lists every schema key per subpath, not because the keys are authorable") is exactly the reading the gate refuses by name. Precedent on main for the same shape — a new request/response schema in api/ that moved this shard — is uniform: #28440 (9b1dc99), #28447 (462935d), #28595 (519089e), #27232 (8bb2c31), #27063 (f9f27a0) all shipped Clause-②: yes (widening), Metadata-protocol: added, @objectstack/spec: minor. The card's own text says Clause-②: yes (a response contract widens). Required: in the PR body AND the changeset, Clause-②: yes (widening), Metadata-protocol: added, and @objectstack/spec graded minor (the gate needs at least one moved package at minor; plugin-hono-server and plugin-sharing may stay patch). Not the reverse: no edit to the shard. The gate re-reads the body on edited, but the changeset file needs a push.

Must-fix (declaration). The required hasAnyQueue member on the published IOwnershipQueueService is a break for every out-of-tree implementer (class X implements IOwnershipQueueService stops compiling), shipped under patch with no (narrowing) arm, no BREAKING banner and no ADR-0087 disposition. AGENTS.md: "(narrowing) is BREAKING"; ADR-0087 D7 treats a published runtime interface as compiler-carried and counts "a member added … re-typed or made optional" as a surface change. The repo's practice for a member added to a published contract is OPTIONAL: declaredQueueNamed? on this very interface (a9f654e, ADR-0142) and getRecordFieldAccess? on ISecurityService (f9f27a0, yes (widening)). The route itself still guards typeof hasAnyQueue !== 'function' — the only consumer treats the member as optional, so the contract as written and the code that reads it disagree. Two honest shapes: (a) hasAnyQueue?(…) optional, mirroring declaredQueueNamed?, with the docblock saying a service without it answers false at the carrier (the route already does); or (b) keep it required and declare the break — (narrowing) arm, BREAKING banner, ADR-0087 disposition, migration line ("add hasAnyQueue(organizationId); return false to keep today's wire"). Reviewer's recommendation is (a): the ruling record asked for the method on the contract and the route reading it through the contract, which (a) satisfies with Pick/Partial exactly as the head already compiles, and it leaves the PR a pure widening that then declares yes (widening) consistently with the first must-fix.

skip-changeset is not in question (three released packages move). With both fixes the level is @objectstack/spec: minor, the two plugins patch, Clause-②: yes (widening), Metadata-protocol: added.

③ Boundary flags

  • open_questions[0] "is a required (not optional) hasAnyQueue acceptable as Clause-② yes with no (narrowing) arm, given the only implementer is in-tree?" — No. The interface is published for the out-of-tree case; "only implementer is in-tree" is a fact about today's tree, not about the contract. Answered as must-fix in ②, resolution (a) recommended.
  • Deviation 1 / out_of_scope_findings (no @objectstack/client method; the route sits in no route ledger). Verified: client-url-conformance.test.ts unions five ledgers (runtime, rest, storage, i18n, auth); auth-route-ledger.ts enumerates better-auth routes and /auth/me/permissions is objectstack-mounted by plugin-hono-server, not better-auth; runtime's ledger has only the * /auth/** sdk row (line 623). So a typed client method is refused by design today. Card Target 4 says "The console reads it from the typed client" — this PR meets the declaration half and NOT the typed-client half. Gap to file (should-fix, follow-up card, server lane): add GET /auth/me/permissions (and /auth/me/localization, /me/apps, the same supply) to a route ledger so @objectstack/client can carry auth.mePermissions() under conformance. Until then List toolbar: offer the "In a queue" toggle only when the organization has at least one queue — today every owned object shows it, and in an app with no queues it always returns an empty list #28428 types its fetch with MePermissionsResponse from @objectstack/spec/api, which is workable.
  • Deviation 2 (required member) — answered above.
  • Deviation 3 (objects entries keep tolerance) — accepted as a disclosed limit (① item 3).
  • Deviation 4 (Iso_ pins) — accepted on the pin's mechanism (① item 8).
  • Deviation 5 (no labels) — not reviewed here.
  • Metadata-protocol none "by reading" — refused by the gate on the head; answered in ②.
  • Ablation B not measured — accepted: A (strictness) and C (route sends an undeclared key: 7/9 route cases and 5/5 door cases red) are the two readings that matter for "bound, not declaration-only"; the gen:schema baseline refusing a key rename is itself evidence the key is governed.
  • Dev flag "wire byte-for-byte unchanged" — holds on reading (① item 9).
  • No escalation needed: nothing here needs the maintainer; both must-fixes have a mechanical resolution inside the dev's own lane.

④ Design judgment

packages/spec/src/api/auth.zod.ts — judged against ruling record 6557794880 ("a strict response schema for the /auth/me/permissions body (anonymous, degraded, and full/closed shapes, hasAnyQueue: boolean declared)") and its first consumer, the route plus the #28428 console.

  • Holds against the route: every key, every type, the optional pair's absent-vs-empty distinction, and the discriminator (① item 2). The design choice to make absent systemPermissions a declared state ("unknown, fail open") rather than defaulting it is right: a .default([]) would have turned the degraded body into the closed one on parse, which is the exact conflation finding(plugin-hono-server): the /auth/me/permissions outer catch answers without systemPermissions, which the console reads as "capabilities unknown" and fails open #23047 forbids, and it is why the schema is isomorphic and the types are z.input.
  • Tried to break it: (i) the closed body is tenantId: identity.tenantId where identity.tenantId = execCtx.tenantId ?? null — nullable holds; (ii) userId is execCtx.userId guarded truthy — z.string() holds; (iii) a tabPermissions value outside the four cannot reach the wire (tabRank filter) — enum holds; (iv) an objects entry carrying allowRestore: true would be REFUSED by acceptRetiredDefaultResidue while allowRestore: false is stripped — buildEffectiveObjectPermissions emits only GRANT_BITS plus viewAllRecords/modifyAllRecords/apiOperations, never the retired pair, so no live body trips it; (v) the ui reader (MePermissionsProvider.tsx) declares roles: string[], userId: string | null, authenticated: boolean and reads data?.roles ?? [] — the route has sent positions, not roles, since ADR-0068, so the ui's hand-written interface was already drifted; this PR does not break it (the ui does not compile against the spec type), but the drift is the very reason the card asked for a declared contract. Should-fix (follow-up on List toolbar: offer the "In a queue" toggle only when the organization has at least one queue — today every owned object shows it, and in an app with no queues it always returns an empty list #28428, ui lane): the console reads the typed MePermissionsResponse from @objectstack/spec/api and drops its own MePermissionsResponse interface (same name, different shape — a name collision waiting for the import); note roles is dead on the wire.
  • Should-fix (note in the schema, no code change required to pass): GetEffectivePermissionsResponseSchema in api/protocol.zod.ts (non-strict, objects + systemPermissions only, backing an unimplemented optional protocol method) is now a second, weaker declaration of the same concept. The new schema's docblock should name it and say which one the wire is, or a later reader will "unify" them the wrong way. The dev noted it in the server-half report; it is not yet written down in the spec.
  • The hasAnyQueue description is complete and correct against the implementation (declared-and-active OR authored row not shadowed by a declared name; false with no service; false on the closed body) and against the card's non-disclosure rule (no names, no counts) — the route test "the value on the wire is a boolean and nothing else" pins it.

packages/spec/src/contracts/ownership-queue-service.ts — judged against the ruling record and the implementation on main (plugin-sharing line 222).

  • The docblock's semantics are exactly the implementation's; the cross-reference to the carrier schema is right. organizationId: string | null matches the sibling methods.
  • Must-fix: required vs optional, as ② states. The interface header promises "every queue-dependent ADMISSION fails closed when the service is not registered"; this member is not an admission, and the route's own comment says an older implementation "answers false" — that is the semantics of an optional member, and the contract should say what the code does. If the maintainer prefers required, the break must be declared, not shipped as patch with no arm.

skills/objectstack-api/references/_index.md — judged against the generator (build-skill-references.ts, skill-map-guards.ts) and the skill it serves on main. The file is generated and in sync (the dev ran check:skill-refs green; CI's docs lint is in progress). The content is accurate. Should-fix: the one new import drags 22 transitive pointers into the API skill's index, most of them another skill's whole subject (ui/view.zod.ts, ui/chart.zod.ts, ui/bulk-action.zod.ts, shared/conditional-formatting.zod.ts, data/validation.zod.ts …). The repo already has the instrument for exactly this — TRANSITIVE_ALLOWLIST in skill-map-guards.ts, whose objectstack-platform entry exists because "everything else the closure reaches here is … a different skill's whole subject". An objectstack-api row that keeps security/permission.zod.ts (the one file an agent reading MePermissionsAuthenticatedResponse.objects must follow) and prunes the rest keeps the index a set of pointers an agent can use, which is the generator's own stated standard ("pointing at a schema the body cannot help with sends an agent into node_modules for a lesson that does not exist"). Not a FAIL on its own: the index is true, just bloated.

Summary of findings. Must-fix: (1) changeset/declaration level — Metadata-protocol: added, Clause-②: yes (widening), @objectstack/spec: minor, as the red Check Changeset gate and five precedents require; (2) hasAnyQueue required on a published contract is an undeclared implementer break — make it optional like declaredQueueNamed? (recommended) or declare the (narrowing) break with its carriers. Should-fix: route-ledger gap for a typed client method (file it); #28428 reads the spec type and retires the ui's drifted MePermissionsResponse (roles); cross-reference GetEffectivePermissionsResponseSchema from the new schema; an objectstack-api transitive allowlist to prune the 22 new skill pointers.

Implemented-by: claude/project-thread-7xha1v
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: FAIL

…nge is a widening that adds to the protocol (spec minor)

Contract review on 567e3dc: a required member is an undeclared break for an out-of-tree implementer, and the route already guards a missing method. The changeset declares Clause-② yes (widening) and Metadata-protocol added, with @objectstack/spec at minor, since the authorable-surface baseline moves. The objectstack-api skill index keeps its pre-#28508 transitive pointers through a TRANSITIVE_ALLOWLIST row instead of gaining 22 through the new permission.zod edge. The schema's doc names the older GetEffectivePermissionsResponseSchema it is not.

Co-Authored-By: Claude <noreply@anthropic.com>
…contract, not hand-written

@object-ui/permissions re-exports MePermissionsResponse (and MePermissionsAuthenticatedResponse) from @objectstack/spec/api. The hand-written interface had drifted from the wire: it read a roles key the server never sends, so usePermissions().roles under MePermissionsProvider was always empty; it now carries the server's positions. Only the authenticated body's grants are read. Test fixtures typed as the response move roles to positions and add hasAnyQueue; the anonymous fixture is { authenticated: false }.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/m labels Nov 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0eb64410bfc2f25acf1f9b8985675654342f89ff
Local-runs: none
Scope: packages/spec/src/api/auth.zod.ts, packages/spec/src/contracts/ownership-queue-service.ts, skills/objectstack-api/references/_index.md + .changeset/28508-me-permissions-response-contract.md, packages/spec/scripts/lib/skill-map-guards.ts, packages/plugins/plugin-hono-server/src/current-user-endpoints.ts, ui/packages/permissions/src/MePermissionsProvider.tsx, ui/packages/permissions/src/index.ts, ui/packages/permissions/src/PermissionContext.ts, ui/packages/permissions/src/usePermissions.ts, ui/apps/console/src/components/studioEntry.ts, ui/scripts/check-spec-symbol-derivation.mjs, the 121 ui test files the round-2 diff migrates (git diff 567e3dc8 0eb64410), skills/objectstack-api/SKILL.md, read against the base record (PR comment 6559372294 on 567e3dc8), the live PR body, claim amendment 6560444824 and card #28508.

Delta record. Round 2 is two commits over the base head (d86253262, 0eb64410b): 129 files, +591/−469, of which 8 are source and 121 are ui test fixtures. Everything the base record judged and does not name below stands unchanged on this head (the schema, its binding to the route and door tests, the wire, the generated artifacts, the plugin-sharing docblock, the verify test): none of those files moved between the two heads.

Check-runs on 0eb64410 at read time (46): Check Changeset ran twice — the first run (job 124902013218, started 16:07:30Z on the synchronize event) completed failure with the SAME none-baseline-moved refusal as the base head, because it read the PR body as it stood before the seat's 16:08Z edit (Clause-②: yes, Metadata-protocol: none); the second run (job 124902297495, started 16:08:34Z on the edited event, the body now yes (widening) / added) completed success on every step, "Guard against accidental major bumps" included. The gate re-reads the body on edited by design, so the newer run is the gate's verdict on this head. Also completed success: Governed Surface Queue Guard, PR-body guards, Check PR Size, Lint · ESLint, Lint · docs & code guards, Lint · scoped · query-options erasure, Lint · scoped · tooling self-tests, Type Check · source gates, Spec property liveness, Dogfood Regression Gate, Plan Test Core shards, Flag docs affected, Check Documentation Links, Auto Label, filter. Still in_progress: Test Core (1..8/8), Build Core, Build Docs, Build UI Deps, Live E2E UI, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint · scoped · slot lookup & corpus. The ui typecheck, check:spec-symbols (ci.yml line 3599) and the ui suites are therefore CI's still; the dev's readings (13 packages 0 errors, 1761/1761, 115/115 gates) are the dev's account.

① Derived judgments

Delta over the base record's twelve items:

  • Item 4 (contract member), re-judged right. hasAnyQueue?(organizationId: string | null): Promise of boolean is now OPTIONAL on IOwnershipQueueService, with a docblock naming declaredQueueNamed as its precedent and the route's false for an implementation without it. An implementation written against the published 18.x contract still compiles; the route file is unchanged from the base head and its Pick + Partial read with the typeof guard is exactly the shape an optional member calls for. No generated artifact moves (contracts are TS interfaces; api-surface/contracts.json lists the symbol only).
  • Item 12 (changeset), re-judged right. .changeset/28508-me-permissions-response-contract.md: @objectstack/spec: minor, @objectstack/plugin-hono-server: patch, @objectstack/plugin-sharing: patch, @object-ui/permissions: patch; body lines Clause-②: yes (widening) and Metadata-protocol: added; the live PR body carries the same two lines. This is the shape the base record required and the precedent set (feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape #28440, feat(spec): declare the publish-drafts request body, so the change note is part of the published API contract #28447, feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595, feat(security): the record field-access answer says whether the record itself is writable #27232, feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063) ships. The newer Check Changeset run confirms it. The shard packages/spec/authorable-surface/api.json is untouched between the heads: the baseline was not edited to quiet the gate.
  • Item 11 (skill index), re-judged right. skills/objectstack-api/references/_index.md is now byte-identical to origin/main (git diff origin/main on the path is empty): the new objectstack-api row in TRANSITIVE_ALLOWLIST publishes the 13 transitive pointers the index already carried and nothing the closure newly reached. See ④ on whether the row is the right fix.
  • New public-surface change: @object-ui/permissions (published, 18.2.0). The exported MePermissionsResponse stops being a hand-written interface (authenticated: boolean, userId: string | null, roles: string[], loose objects/fields) and becomes a re-export of the spec's discriminated union; MePermissionsAuthenticatedResponse is re-exported beside it (index.ts). The provider narrows once (authed = data?.authenticated === true ? data : null) and reads fields, objects, userId, systemPermissions, effectiveObjects and (new) positions from the authenticated branch only; studioEntry.ts narrows on authenticated before reading systemPermissions. Right as a derivation: it is the form check-spec-symbol-derivation.mjs sanctions ("export type { X } from '@objectstack/spec/…'"), and the spec export of a symbol named MePermissionsResponse would otherwise have made the ui's same-named interface an untriaged collision for that gate. It is a type-level change only; no runtime read moved except roles (③).
  • New: 121 ui test files migrated. Read as a whole through the diff's non-mechanical residue: every changed line is one of (a) type MePermissionsResponse annotations and imports becoming MePermissionsAuthenticatedResponse on builders that answer authenticated: true, (b) roles: [...] becoming positions: [...], (c) hasAnyQueue: false added, (d) anonymous fixtures collapsing to { authenticated: false } (the only shape the strict anonymous branch admits), (e) four fixtures whose field entries were { readable: false } gaining editable: false. Nothing else. Right: a fixture typed as the response must now be a body the server can send.

② Semver level

Both base-record must-fixes are resolved in the tree and in the declarations: @objectstack/spec at minor with Metadata-protocol: added and Clause-②: yes (widening) in body and changeset; the contract member optional, so the PR is a pure widening and the arm is consistent with the diff. Check Changeset on the live body: success. check-adr-0087-registration (step "Require an ADR-0087 disposition"): success, "adds no declared-breaking changeset" — right, nothing in the diff removes or renames a published spec surface.

@object-ui/permissions: patch — noted for the lane seat, not a contract-tier finding: the package's exported MePermissionsResponse type narrows for a consumer (a union now; roles gone; userId reachable only after narrowing on authenticated), which is a compile-time change for an out-of-tree reader of that type. The base record held the spec's contracts/ interface to the stricter standard (must-fix 2) because that is the published contract face this review governs; the ui type was a hand-written fork of a wire shape that the repo's own gate forbids, and replacing it with the derivation is the gate's prescribed remedy (its header lists the same move for ActionType, ChartType, ActionLocation). The changeset body already carries the roles → positions mapping; should-fix (lane seat, wording only): add one line that the type is now the discriminated union and a reader narrows on authenticated before reading grants, as studioEntry.ts had to. patch stands.

③ Boundary flags

  • Must-fix 1 (declarations) — resolved; verified on the live body, the changeset blob and the newer Check Changeset run. The dev's local red/green (old body exit 1, new body exit 0) matches CI's two runs in order.
  • Must-fix 2 (required member) — resolved: optional, docblock says why, route unchanged and already in the optional shape.
  • Should-fix (a), allowlist row — resolved for the reader (index identical to main). Judgement in ④.
  • Should-fix (b), cross-reference — resolved: the MePermissionsAuthenticatedResponseSchema docblock names GetEffectivePermissionsResponseSchema (api/protocol.zod.ts) as the older, non-strict answer of the optional getEffectivePermissions protocol method with "no route serves it". Verified: no rest, runtime or plugin-hono-server source mounts a route for it, and packages/client names the schema only in type position with no URL. Accurate.
  • Should-fix (c), ui reads the typed contract — resolved in this PR rather than on List toolbar: offer the "In a queue" toggle only when the organization has at least one queue — today every owned object shows it, and in an app with no queues it always returns an empty list #28428 (the base record filed it there). Judged right to ship here, not split: the spec export MePermissionsResponse and the ui's hand-written MePermissionsResponse collide by name, and check:spec-symbols runs in this repo's CI on every PR (ci.yml line 3599), so a spec-only landing would have left main red on that gate or needed a throwaway triage entry; AGENTS.md Post-Task Checklist step 4 is the same principle for the removal case ("a removal that breaks ui/ ships together with the ui/ fix — or it does not ship"). The seat's claim amendment 6560444824 declares the ui/** surface, so the order's original ban is lifted on the record. The widening is bounded: two ui source files, one console file, fixtures.
  • Should-fix (d), route-ledger gap / typed client method — not in this PR; the PR body says "The seat files the typed client method as its own card". Still owed as a card; not a finding on this head.
  • Dev flag: "roles now carries the server's positions, where it was always empty before." Judged: no gate or rendering changes. The only readers of PermissionContextValue.roles in ui production code are usePermissions.ts (the frozen no-provider NO_ROLES default) and the legacy role-based PermissionProvider, which fills roles from its own config and does not read MePermissionsProvider's context; no ui/packages/*/src or ui/apps/*/src non-test file reads .roles off the permission context. So the value moves from [] to the caller's positions with nobody reading it. Right, and the dev's own disclosure is accurate. Should-fix (ui lane, follow-up): the context key is now a misnomer (roles carrying ADR-0068 positions); deprecate it or rename to positions on a ui card, since a future reader will take the name at face value.
  • Dev flag: three (read: four files) { readable: false } entries gain editable: false. In scope and right: the strict fields entry requires both keys, the server always sends both (buildEffectiveFieldPermissions returns { readable: boolean; editable: boolean }), and the provider answers write as fieldPerm.editable !== false, so a read-denied fixture without editable was a shape the wire never carries that answered write=true. The four tests (approvalRequestsDataSource.servedPageSummary-12483, …Amount-12534, RecordPickerDialog.fieldReadRule-10594, MePermissionsProvider.test) assert the read side (queryByText(raw) null, write false), which the change does not move.
  • Dev flag: anonymous fixtures collapse to { authenticated: false }. Right: the provider's anonymous path already ignored grants (authed is null), and the strict anonymous branch admits nothing else. A helper that built { authenticated: false, userId: null, objects, fields } now drops objects/fields for the anonymous case — the body the server actually sends.
  • Dev results (115/115 gates, 13 packages typecheck 0, 1761/1761, spec/hono/sharing/door green) — the dev's account; CI's Type Check · workspace, Type Check · consumer gates, the ui lint job carrying check:spec-symbols and the Test Core shards are in progress at read time and are the verdicts of record for those families.
  • No escalation. Nothing here needs the maintainer.

④ Design judgment

packages/spec/src/api/auth.zod.ts — unchanged except the cross-reference paragraph; the base record's judgement stands (holds against every body the route answers; bound by tsc and the route and door tests; the objects entries' tolerance is a disclosed limit). The new paragraph is accurate (③) and does what the base record asked: a later reader cannot "unify" the two schemas the wrong way.

packages/spec/src/contracts/ownership-queue-service.ts — hasAnyQueue? optional. Holds against the implementation (plugin-sharing line 222, unchanged), against the route (which already answers false for a service without it and keeps the typeof guard), and against the interface's own doctrine: the member is not an admission, so "absence is not a fault, and never a grant" applies and false is the right answer for an older implementation. Consistent with declaredQueueNamed? on the same interface and getRecordFieldAccess? on ISecurityService. No must-fix.

skills/objectstack-api/references/_index.md and the TRANSITIVE_ALLOWLIST row — is the row the right fix or a workaround? Right fix, with one honest caveat. The guard file's own design note rules out the alternative: a reachability rule over the import graph was measured as unbuildable ("route 1 … is unbuildable from this input, and route 2 is what ships"), and the allowlist exists for exactly this event — "shared/value-domain.zod.ts joined the i18n index unnoticed when a new import edge appeared several files away. An allowlist cannot silently gain a row." A row is also the only way to keep the pointer set stable while auth.zod.ts legitimately imports security/permission.zod.ts (keeping a type-only import alive to steer the generator is the shape the note refuses). The row passes every mechanical check the guard asks (13 shipped *.zod.ts files, none a core entry, none twice). The caveat: the note says a row "means what its comment says — the SKILL.md teaches this schema — … only a package whose face someone has actually read should carry one", and the dev's row comment says instead "the closure as it stood before #28508 … the transitive pointers the index already published": a freeze of the old closure, not a read of the skill's face. That is honestly worded and leaves the reader exactly where main left them, so it is not a regression; but security/explain.zod.ts, kernel/metadata-protection.zod.ts and shared/suggestions.zod.ts are unlikely to be anything the API skill teaches. Omitting security/permission.zod.ts is consistent: skills/objectstack-api/SKILL.md on main does not teach /auth/me/permissions, hasAnyQueue or EffectiveObjectPermission, so a pointer there would be the "lesson that does not exist" the generator's comment warns against. Should-fix (follow-up, skills lane, non-blocking): prune the 13 rows to what the SKILL.md names, per the guard's doctrine, or amend the row comment to say it is a freeze pending that read.

ui widening (MePermissionsProvider.tsx, index.ts, studioEntry.ts, fixtures) — read, not reviewed at this tier (the lane seat's ACCEPT); the contract-facing question is whether the first consumer now reads the contract as declared. It does: the ui no longer carries a parallel shape, the console narrows on the discriminator the spec declares, and a key the server adds or drops will red the ui typecheck. The roles → positions move alters no user-visible gate (③). No must-fix.

Summary of findings on this head. Must-fix: none — both base-record must-fixes are resolved in the tree and confirmed by the newer Check Changeset run. Should-fix (none blocking): changeset wording for the @object-ui/permissions type narrowing; deprecate/rename the context's roles key on a ui card; prune the objectstack-api allowlist row to what the SKILL.md teaches; the route-ledger card for a typed client method stays owed by the seat. CI families still in progress at read time are named above and remain the verdict of record for what they cover.

Implemented-by: claude/project-thread-7xha1v
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

…n left unused

object-ui/no-unused-imports (Lint UI) flagged 53 test files across 7 packages after their builders moved to MePermissionsAuthenticatedResponse.

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e51961f0ab8b123546f06a079c94829219dbb216
Local-runs: none
Scope: packages/spec/src/api/auth.zod.ts, packages/spec/src/contracts/ownership-queue-service.ts, skills/objectstack-api/references/_index.md + the 53 ui test files git diff 0eb64410 e51961f0 touches (none of the hits paths and no other source file moved on this head).

Head re-confirm. This record extends the delta record on 0eb64410 (PR comment 6560520274, VERDICT PASS), which itself extends the base record on 567e3dc8 (comment 6559372294). One commit over the delta head: e51961f0a test(ui): drop the MePermissionsResponse imports the fixture migration left unused.

What the diff holds, read in the tree. 53 files changed, +53/−53, every one a *.test.tsx under ui/packages/ (app-shell 32, plugin-detail 9, plugin-grid 4, plugin-list 3, permissions 3, fields 1, plugin-view 1). Every changed line is one import statement losing the type MePermissionsResponse specifier that the round-2 fixture migration left unused (the fixtures now type their builders as MePermissionsAuthenticatedResponse): 47 lines from '@object-ui/permissions', 3 from '../MePermissionsProvider', 2 that also import usePermissions / PermissionProvider, 1 multi-line specifier list. No other token moves: no fixture value, no assertion, no source file, no spec file, no generated artifact, no changeset. The dev's account of the commit is exact.

What it answers. Lint UI on 0eb64410 (job 124903505086) completed failure on object-ui/no-unused-imports over these specifiers; the delta record named that lint job as in progress and as the verdict of record for its family. This commit removes the unused specifiers and nothing else, so the delta record's judgements — ① through ④, the two resolved must-fixes, the should-fixes — stand on this head unchanged.

Check-runs on e51961f0 at read time (44). Completed success: Governed Surface Queue Guard, PR-body guards, Check PR Size, Auto Label, Spec property liveness, Dogfood Regression Gate, Plan Test Core shards, Flag docs affected, Check Documentation Links, filter. Still in_progress: Check Changeset (the live body and changeset are unchanged from the delta head, where the edited-event run passed), Lint · ESLint, every other Lint · job, Type Check · source gates / workspace / consumer gates / debt ledger, Build Core, Build Docs, Build UI Deps, Live E2E UI, Test Core (1..8/8). The rest skipped (opt-in and matrix placeholders). So the Lint UI family this commit exists to green is not yet answered by CI on this head; nothing completed is red.

① Derived judgments

No accept-set or public-surface change on this head beyond the delta record's: the diff touches test files only.

② Semver level

Unchanged: @objectstack/spec: minor, Clause-②: yes (widening), Metadata-protocol: added; the two plugins and @object-ui/permissions at patch. Test-only removals publish nothing.

③ Boundary flags

Dev flag "test-only removals of an unused import from 53 ui test files, no other change" — verified exactly (above). No open question, no escalation.

④ Design judgment

Nothing in the hits files moved. The delta record's ④ stands.

Implemented-by: claude/project-thread-7xha1v
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28704 · 2026-11-01T17:03Z

Head-sha: e51961f0ab8b123546f06a079c94829219dbb216

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28508(服务端一半 PR feat(plugin-hono-server,plugin-sharing): /auth/me/permissions answers hasAnyQueue from the ownership-queue service #28654 已合入,本 PR 收尾;Target 4 的 client 方法另立 client: GET /auth/me/permissions has no route-ledger row, so packages/client cannot offer a typed method for the now-declared MePermissionsResponse #28709,本 PR 合入后解除阻塞),合并关卡正确。143 个文件,在认领 6556788298 与修订 6557730251、6560444824 申报面内;ui/** 扩面由修订申报:spec 导出与 ui 手写接口同名,check:spec-symbols 要求同 PR 迁移(D5),复核认定不应拆分。
  2. 前提实测:dev 实测每个真实响应体过严格 schema(hono-server 9 例)、契约测试 12 例在 main 上红;漂移的 roles 键实测无服务端发送者。
  3. 门禁:本 head check-runs 58 success、7 skipped(路径未命中)、0 失败或待跑;0eb64410 上的 Lint UI 红(未用导入)已由 e51961f0 修复,本 head Lint UI success。dev 本地 115 条派生门禁全部 exit 0,0 NOT-MEASURED;13 个 ui 包 typecheck 0 错,ui 测试 1761/1761,spec 23185、hono-server 466、sharing 1302 通过。
  4. 接受的偏差:(1) skill 索引用 TRANSITIVE_ALLOWLIST 冻结 main 已发布的 13 条传递指针,复核认定是正解(守卫文件为此场景设立),后续可裁剪到 SKILL.md 实际所教;(2) 四个夹具文件补 editable: false,严格条目要求该键且线上一直发送,在范围内;(3) @object-ui/permissions: patch(导出类型收窄为判别联合,属门禁规定的漂移修复)。
  5. 范围外去向:非阻断后续:裁剪 allowlist 行;弃用/改名误名的上下文 roles 键;正文措辞点明类型现为判别联合。client: GET /auth/me/permissions has no route-ledger row, so packages/client cannot offer a typed method for the now-declared MePermissionsResponse #28709(client 方法)合入后解除 pm:blocked。
  6. 落地条款:Clause-②: yes (widening) + packages/spec 路径 ⇒ CONTRACT_REVIEW_TIER 完整复核:基础记录 FAIL(6559372294,567e3dc8)→ delta PASS(6560520274,0eb64410)→ head 复核 PASS(6560826611,Head-sha 与现 head 一致;e51961f0 仅删去 53 个 ui 测试文件中未用的类型导入,修复 Lint UI)。Metadata-protocol: added ⇒ spec minor,与 changeset 一致(Check Changeset 本 head 最新一次 success;16:07Z 那次 failure 读的是旧正文)。check-governed-merges --branch claude/project-thread-7xha1v:NOT governed,1486 行。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 17:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 17:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 4d278c9 Nov 1, 2026
66 checks passed
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/xl tests tooling

Projects

None yet

1 participant