Skip to content

refactor(spec,client)!: retire the notification-list cursor on both halves and drop the fictional limit default (#6361) - #6866

Draft
os-zhuang wants to merge 5 commits into
mainfrom
claude/issue-6361-notifications-cursor-removal
Draft

refactor(spec,client)!: retire the notification-list cursor on both halves and drop the fictional limit default (#6361)#6866
os-zhuang wants to merge 5 commits into
mainfrom
claude/issue-6361-notifications-cursor-removal

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #6361

GET /api/v1/notifications declared cursor on both halves of its contract and honoured it on neither. The dispatcher domain reads read / type / limit and nothing else, and no emit site has ever written the response key — so a caller paginating by the published contract re-read the first window forever, with no error and no 400. Both pages parsed green against the response schema, which is why no conformance gate could see it.

It was worse than inert because it had a shipped producer: the SDK appended cursor to the query string, so the dead parameter was reachable from ordinary typed code. This is data.query.cursor (#4286, query-cursor-retired) one layer up, with the same verdict for the same reason — down to deleting the SDK producer alongside the key.

Ruled jointly with #6363 (maintainer ruling 2026-08-07, Option A): one capability's two halves are never half-deleted. #6363 made its declaration true; this one removes a declaration there was no implementation to make true about. Opposite repairs, one rule.

Premise re-verified before implementing

Measured on origin/main@6de592c (not taken from the issue body):

Issue claim Verdict
request cursor at protocol.zod.ts ~:1158 ✅ holds
response cursor at ~:1164 ✅ holds
client appends cursor at ~:3809 ✅ holds
runtime reads only read/limit/type ✅ holds
"DEFAULT_NOTIFICATION_ROUTES declares ListNotificationsRequestSchema" does not hold — see below

The one correction: the route catalog entry for GET '' carries no requestSchema at all (only responseSchema). #3899 stripped the notification table's request schemas. The declaration that misled callers lives in protocol.zod.ts itself — the exported schema plus the listNotifications(request: ListNotificationsRequest) protocol method — not in the catalog. The defect is real; that one sentence of its explanation was not.

What changed

  1. cursor is tombstoned, not deleted, on both schemas. This is the one substantive departure from the dispatch card's wording, and the gate is what taught me: check:authorable-surface refuses a bare deletion because neither schema is .strict(), so deleting the key would make Zod silently strip whatever callers keep sending — a clean parse and a parameter that never takes effect, i.e. this very defect re-created one layer down (ComputedFieldCacheSchema 是 2026-06 字段剪除留下的第二个孤儿(#3726 表格误记为「已清理」) #3733, ADR-0104). retiredKey() types the key never and raises the prescription at parse time. Both keys registered in RETIRED_KEYS_BY_MAJOR[17].
  2. cursor removed from client.notifications.list(). Passing it is now TS2353.
  3. limit loses its .default(20) (the ruling's second arm). Rationale below.
  4. ADR-0087 D3 entry notification-list-cursor-retired. No D2 conversion: a conversion rewrites an authored source or a stored sys_metadata row, and both shapes are HTTP-only — nobody authors a ListNotificationsRequest and nothing persists one. Same disposition BatchOptions.validateOnly (BatchOptions.validateOnly 声明了 dry-run 但从不实现 —— "预演"会真实落库(PD #10) #4052) already takes in this major.
  5. DEFAULT_CHANGES_BY_MAJOR[17] — this table's first entry ever; it shipped empty at 17 and a second gate (spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666) demanded the declaration.
  6. Route catalog description no longer says "paginated" — a machine-readable surface must not advertise a capability that does not exist (Route and surface ownership rule 4).

The limit trade-off, as asked

The ruling allowed either declaring the real server default (50) or dropping it as server-decided. Dropped, because the fiction was the mechanism, not the number: nothing parses this query string through the schema (#3899 wired requestSchema to the real entry for bodies only), so .default(20) never stamped anything onto anything. Re-spelling it 50 keeps a declaration that does not execute and merely makes it coincide with the implementation until someone moves the clamp — a fiction with a truer number is still a fiction.

Cost of the choice, stated plainly: ListNotificationsRequestParsed.limit becomes number | undefined instead of number, so a consumer reading the parsed shape loses a guaranteed number. That is the honest type — the window is the server's answer, not the schema's.

No .int(), .positive() or .max(200) is declared either: the service clamps an out-of-range limit rather than refusing it, and declaring a rejection the wire does not perform is the same defect mirrored.

Wire behaviour is UNCHANGED — measured, as required

A request still carrying ?cursor= is ignored, not refused. The route reads three named query keys and validates no query against a schema, so an unknown key never produced a 400 and does not start now. Asserted over a real socket in the integration suite, and at the domain level in the unit suite. An omitted limit still yields the server's 50-row window. unreadCount is #6363's landed business and is untouched.

Reverse verification (direction predicted first)

  • Predicted RED, got RED: a probe calling list({ cursor: 'n_42' })error TS2353: Object literal may only specify known properties, and 'cursor' does not exist in type '{ read?: boolean | undefined; type?: string | undefined; limit?: number | undefined; }'. Probe deleted.
  • Predicted RED, got RED: temporarily restoring cursor: z.string().optional() on the request half turned the new spec pin red (1 failed | 34 passed) and the @ts-expect-error pin red (src/api/protocol.test.ts: 1 type error(s)). Restored. Both pins are load-bearing rather than decorative.

Verification (re-run after merging origin/main)

main moved twice under this branch; both merges conflicted in packages/spec/src/migrations/registry.ts because #6862 and a sibling retirement appended to the same two tables. Resolved by keeping both sides in both places, then regenerating — never hand-merging — the driver-deferred artifacts. Asserted present after resolution: action-descriptor-is-async-retired and notification-list-cursor-retired; automation/ActionDescriptor:isAsync and both api/ListNotifications*:cursor keys.

  • pnpm --filter @objectstack/spec check:generatedAll 10 generated artifacts are up to date.
  • Tests, post-merge: spec 8936 passed (348 files) · client 272 passed (21) · runtime 1741 passed (115) · service-messaging 199 passed (16).
  • typecheck green on all four packages.
  • Gate list enumerated from lint.yml, run locally one by one: 52 passed, 0 failed.
  • Four gates could not be measured locally and were left to CI rather than counted as passes: check:type-check-debt, check:i18n, check:i18n-coverage and check:app-nav-i18n each answered PREREQUISITE NOT MET ("Nothing was checked") because they refuse to run without a fully built workspace, and the container's shared build lock was seven agents deep. CI has since run all four on a clean checkout: ESLint and TypeScript Type Check both concluded success, along with every other job.
  • CI converged: 27 checks, 26 success, 1 skipped (Console Pin Gate), 0 failures.

Consumer sweep

Prefix direction (...@objectstack/spec, ...@objectstack/client). The only cursor reader on this route was the SDK's query append. objectui's useClientNotifications.ts calls list({}) — unaffected. cloud has no reference. Every pin of the old shape was flipped to assert the new substance, not merely to drop the old assertion.

Effect on #3877 Stage D (mandatory answer)

Unchanged — and the card's own evidence gets sharper. The Stage D double-assertion ratchet still cannot see either fact: cursor was optional, so "no producer emits it" was always a legal parse and the KEY assertion (subset, not equality) could not see it; removing an optional key changes no parse verdict either. Both the defect and the fix had to be written by hand, which is precisely the Stage D input recorded on #6363, now demonstrated in both directions — #6363 repaired by making a declaration true, #6361 by removing one. No other notification-family card is made easier or harder; the retired key is registered where Stage D can read it if the ratchet ever grows a declaration-level input.


Generated by Claude Code

… halves and drop the fictional `limit` default (#6361)

`GET /api/v1/notifications` declared `cursor` on the request AND the response
schema and honoured it on neither: the dispatcher domain reads `read`/`type`/
`limit` and nothing else, and no emit site ever wrote the response key. The SDK
appended it to the query string, so a caller paginating by the published
contract re-read the first window forever — no error, no 400, both pages
parsing green against the response schema.

Maintainer ruling 2026-08-07 (Option A), ruled jointly with #6363: one
capability's two halves are never half-deleted.

- `cursor` is tombstoned via `retiredKey()` on BOTH schemas — not deleted.
  Neither is `.strict()`, so a bare deletion would have made Zod silently strip
  what callers keep sending, re-creating this very defect one layer down
  (#3733, ADR-0104). Registered in RETIRED_KEYS_BY_MAJOR[17].
- The `cursor` parameter is removed from `client.notifications.list()`; passing
  it is now TS2353.
- `limit` loses its `.default(20)` rather than being re-spelled 50: nothing
  parses this query through the schema (#3899 wired requestSchema for bodies
  only), so the default never executed at any value. Declared in
  DEFAULT_CHANGES_BY_MAJOR[17] — this table's first entry.
- ADR-0087 D3 entry `notification-list-cursor-retired`; no D2 conversion (both
  shapes are HTTP-only — nothing authors or stores one).

Wire behaviour is deliberately UNCHANGED: `?cursor=` is still ignored, not
refused, and an omitted `limit` still yields the server's 50-row window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 4:40am

Request Review

claude added 2 commits August 9, 2026 03:14
…ifications-cursor-removal

# Conflicts:
#	packages/spec/src/migrations/registry.ts
…ifications-cursor-removal

# Conflicts:
#	packages/spec/src/migrations/registry.ts
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/client, @objectstack/spec.

116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/client, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/client)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

`main` gained #6868 (`page-tabs-type-to-tab-style` + `ui/PageTabsProps:type`)
after this branch's last CI head. The merge itself was clean — git placed the
three siblings' entries at different offsets — but the generated artifacts are
driver-deferred and were regenerated from the merged tree rather than
text-merged.

All three protocol-17 siblings verified present after regeneration:
`notification-list-cursor-retired` (this PR), `action-descriptor-is-async-retired`
(#6862) and `page-tabs-type-to-tab-style` (#6868, a D2 conversion).

`authorable-surface/api.json` and `authorable-defaults/api.json` pick up
`api/ApiRoutes:email` and `api/MetadataEndpointsConfig:maskObjectFields` from
other PRs merged in the same window — this branch's copies were simply behind.

check:generated 10/10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
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/l tests tooling

Projects

None yet

2 participants