Skip to content

fix(spec): InboxListResult.unreadCount stops documenting the window count it stopped being (#6438) - #6463

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6438-unreadcount-jsdoc
Aug 7, 2026
Merged

fix(spec): InboxListResult.unreadCount stops documenting the window count it stopped being (#6438)#6463
os-project-manager merged 1 commit into
mainfrom
claude/issue-6438-unreadcount-jsdoc

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6438

The contradiction, and which side was wrong

INotificationService is a published contract: this JSDoc ships in the .d.ts and is the sentence a TS SDK consumer reads in their editor. The unreadCount member said:

Unread count over the returned window.

That recorded the implementation as it stood before #6363. After #6363 (Option A, maintainer ruling 2026-08-07; PR #6439, merged to main as 17d095413), service-messaging counts the total unread across the user's whole matching inbox, and the window bounds notifications[] only. The wire declaration one directory over had been saying that all along:

// packages/spec/src/api/protocol.zod.ts:924
unreadCount: z.number().describe('Total number of unread notifications'),

So one package carried two opposite sentences about one field, with the implementation standing on the .describe() side — and this JSDoc was the last statement of the retired semantics left in the repo. Verified rather than assumed: grep -rn "returned window" over all *.ts / *.md / *.mdx now matches nothing but #6363's own changeset, where the phrase correctly describes what was fixed.

Left alone it is the sentence that teaches the bug back. A consumer told the number is "over the returned window" writes exactly the adaptation #6363 exists to delete — re-counting notifications themselves, or clamping the badge to the page size. On the anti-AI-error axis it is the same point: an AI-written consumer is generated from this JSDoc and nothing else.

What changed

Both members are now documented, because after #6363 their bounds differ on purpose and the interface had never written that difference down anywhere:

  • notifications — the limit-bounded window; one page, implementations may clamp. Worded to match InboxQuery.limit's own existing "Implementations may clamp", so the contract does not bake service-messaging's default 50 / cap 200 into a surface every provider must honour.
  • unreadCount — the total across the whole matching inbox, explicitly NOT the window, anchored to the .describe() text, with the consequence spelled out for consumers ("do not re-derive by counting notifications, do not clamp to notifications.length") and the measured notification 响应侧:unreadCount 声明「总未读数」实测只数 limit 窗口内;响应 cursor 从无 producer 发出 #6363 symptom kept as the reason.

One clause beyond the two sentences the card scoped: InboxQuery.read does not zero the count. Reason it earned its place — "whole matching inbox" alone is ambiguous under read: true, and both in-repo implementations already agree on the answer, so this states an existing fact rather than declaring a new obligation: messaging-service.ts says it outright ("asking for the read half of the inbox does not mean the badge is zero"), and the contract test's own fake computes all.filter((r) => !r.read).length over the unfiltered set, not over the sliced window.

Deliberately not promoted to the contract: how a type filter interacts with the count. That is stated at the implementation level by #6363 and is a per-implementation decision; promoting it here would be a new contract clause, not a truth repair.

Scope

Text only — no schema, no value, no behavior. Every input that validated before validates byte-for-byte after. Nothing request-side (cursor / limit parsing, #6361), no response-side cursor removal, no .zod.ts byte, no content/docs/releases/ byte.

Changeset

Carried, @objectstack/spec: patch — not skipped. This JSDoc ships in the published .d.ts, so it is user-visible in the only sense that matters for a contract package, and it matches the precedent of #6364 (b70e534cc), a JSDoc-truth flip in the same package that carried a patch changeset for the same reason.

Verification

Premise re-verified against fresh origin/main before implementing (the issue is a lead, not a spec) — all three anchors held: #6439 merged (17d095413, the count is now a reverse join over the whole matching inbox), protocol.zod.ts:924 already reads 'Total number of unread notifications', and the stale JSDoc was still present at notification-service.ts:102.

Gate list enumerated from .github/workflows/lint.yml, not from memory — every check:* step of both jobs, run one by one:

  • ESLint job (30/30 pass): lint, check:slot-lookup, check:query-options-erasure, check:nul-bytes, check:doc-authoring, check:docs-audit-scope, check:role-word, check:quick-reference-counts, check:adr-anchors, check:org-identifier, check:authz-resolver, check:service-providers, check:route-envelope, check:error-code-casing, check:wildcard-fallthrough, check:meta-type-normalized, check:init-service-contract, check:durability-log-level, check:startup-registry-verdict, check:objectui-changeset, check:release-notes, check:release-body, check:node-version, check:workflow-status-functions, check:shard-attestation, check:published-files, check:engine-double-contract, check:resume-authority-declared, check:merge-driver, check:spec-parsed-alias.
  • Type Check job (all pass): check:type-check-coverage, check:driver-conformance, check:stall-guard, tsc --noEmit on spec, check:generated --reconcile-only, check:skill-docs, check:spec-changes, check:upgrade-guide, check:authorable-surface, check:docs, check:skill-refs, check:skill-frame-sync, check:skill-compatibility, check:react-blocks, workspace build, workspace typecheck, check:type-check-debt, examples typecheck, downstream-contract typecheck, check:api-surface, check:exported-any, check:dual-source-exports, check:skill-examples, check:doc-formula-expressions, check:i18n, check:i18n-coverage.

check:i18n and check:i18n-coverage first refused to measure ("PREREQUISITE NOT MET — the workspace CLI is not built", then a chain of unbuilt connector packages). Both are prerequisite refusals, not verdicts — the scripts say so explicitly and warn that piping them reads green either way — so they were made measurable by running CI's own workspace build rather than reported as unmeasured. Final: check-i18n-bundles: OK (9 package(s) — all bundles in sync) and check-i18n-coverage: OK (12 config(s), 660 baselined untranslated string(s), none new), both with REAL_EXIT=0 captured unpiped.

Tests: pnpm --workspace-concurrency=2 --filter @objectstack/spec test339 files / 8679 tests passed.

No pin test, stated plainly rather than manufactured. Interface JSDoc has no runtime surface — there is nothing a test can read, so a "pin" here could only assert the file's own text back at itself. The semantics this prose describes are already pinned where they are executable: #6363's service tests and wire-route property test, plus the .describe() string. The repo's JSDoc-pinning precedents (e.g. object-strictness-batch20.test.ts from #6423) pin Zod .describe() values, which are runtime data; this file has none.

Reverse verification does not apply here, and saying so beats inventing it. There is no diagnostic that can change direction: reverting the diff restores the false sentence and every gate stays green in both states, because no gate reads TS doc comments. The check that does discriminate is the grep above — before this change the repo contained one live TS statement of the retired semantics, after it contains zero.

Generated-artifact assumption confirmed, not assumed: git status after the full gate run shows exactly two files (the source and the changeset); authorable-surface.base.json is byte-identical and check:authorable-surface, check:docs, check:skill-refs and check:api-surface all report in sync with no rewrite.


Generated with Claude Code


Generated by Claude Code

… count it stopped being (#6438)

`INotificationService` is a published contract — its JSDoc ships in the `.d.ts`
and is the sentence a TS SDK consumer reads in their editor. The `unreadCount`
member said `Unread count over the returned window.`, which recorded the
implementation as it was BEFORE #6363.

After #6363 (Option A, maintainer ruling 2026-08-07; PR #6439, merged as
`17d095413`) `service-messaging` counts the TOTAL unread across the user's whole
matching inbox and the window bounds `notifications[]` only. The wire declaration
in the same package had said so all along —
`ListNotificationsResponseSchema.unreadCount.describe('Total number of unread
notifications')` (`api/protocol.zod.ts:924`) — so one package carried two
opposite sentences about one field, the implementation standing on the
`.describe()` side, and this JSDoc was the last statement of the retired
semantics anywhere in the repo (`grep -rn 'returned window'` over `**/*.ts` now
returns nothing but #6363's own changeset describing the fix).

Left alone it is the sentence that teaches the bug back: a consumer told the
number is "over the returned window" writes exactly the adaptation #6363 exists
to delete — counting `notifications` themselves, or clamping the badge to the
page size. That holds double for AI-written consumers, which are generated from
this JSDoc and nothing else.

Both members are documented, because after #6363 their bounds differ ON PURPOSE
and the interface had never written that difference down:

  * `notifications` — the `limit`-bounded window, one page, implementations may
    clamp (matching `InboxQuery.limit`'s own existing wording).
  * `unreadCount` — the total across the whole matching inbox, explicitly NOT the
    window, with the "do not re-derive, do not clamp" consequence spelled out,
    plus one clause saying `InboxQuery.read` does not zero it. Both in-repo
    implementations already agree with that clause: `messaging-service.ts` states
    it, and the contract test's own fake computes `all.filter(r => !r.read)` over
    the unfiltered set, not over the sliced window.

Text only. No schema, no value, no behavior: every input that validated before
validates byte-for-byte after, and no generated artifact moves — the reference
docs render from Zod `.describe()` strings, none of which this touches.

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

vercel Bot commented Aug 7, 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 7, 2026 11:23pm

Request Review

@github-actions github-actions Bot added the size/s label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

112 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 @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @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/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/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/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/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/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/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/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/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/spec)
  • content/docs/releases/v17.mdx (via @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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 7, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 7, 2026 23:40
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 7, 2026
@os-project-manager
os-project-manager marked this pull request as draft August 7, 2026 23:42
@os-project-manager
os-project-manager marked this pull request as ready for review August 7, 2026 23:44

Copy link
Copy Markdown
Collaborator Author

Draft/ready churn on this PR, resolved — for the record, so nobody hunts a phantom automation:

  • 23:41Z ready-flip: the PM (this seat, session session_018ffcE95NaMJcL9XJ9VDYgk), after posting the ACCEPT review on spec: InboxListResult.unreadCount 的 JSDoc 仍写「over the returned window」—— 与 #6363 落地后的实现和同族 .describe() 相反 #6438 and reading both gate jobs' success conclusions. Standard landing flow.
  • 23:42Z draft-conversion: the dev agent, acting correctly on its own information — it had not yet received the acceptance context, verified that no repo workflow flips PRs ready, and defensively re-drafted what looked like an externally un-drafted agent PR. Its caution was the right instinct; there is no external automation involved. Both actions share one GitHub identity, which is why the actor field could not disambiguate.
  • 23:5xZ: re-flipped ready and auto-merge re-armed by the PM after the dev's final report landed and reconciled with the review.

Process lesson adopted on the seat (recorded in the seat post): the PM's early-review ready-flip now waits for the dev's final report (or a confirmed-dead dev) — flipping while a dev is alive-and-unreported races its wrap-up, and this PR is the specimen.


Generated by Claude Code

Merged via the queue into main with commit 33e0385 Aug 7, 2026
26 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6438-unreadcount-jsdoc branch August 7, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: InboxListResult.unreadCount 的 JSDoc 仍写「over the returned window」—— 与 #6363 落地后的实现和同族 .describe() 相反

2 participants