Skip to content

fix(client): derive invitation status from the spec's InvitationStatus union - #8076

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-7781-invitation-status-derive-from-spec
Aug 12, 2026
Merged

fix(client): derive invitation status from the spec's InvitationStatus union#8076
hotlong merged 1 commit into
mainfrom
claude/issue-7781-invitation-status-derive-from-spec

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7781

What

organizations.invitations.list()'s row status was hand-written as
'pending' | 'accepted' | 'rejected' | 'canceled' — missing expired,
ObjectStack's own terminal state driven by expiresAt. listMine() was worse:
a bare string. Both are two more hand-copied spellings of the vocabulary
InvitationStatus (packages/spec/src/identity/organization.zod.ts) already
owns — same divergence family as #7726, which had already widened the spec
side to five values (adding canceled) while this file stayed at four and
drifted the other way.

Per the ruling on the issue: derive, don't extend. Both methods now type
status: InvitationStatus, imported from @objectstack/spec/identity
(@objectstack/client already depends on @objectstack/spec and already
imports several other types from it, e.g. ApprovalStatus), so a future value
added to the spec enum reaches the SDK by construction instead of silently
diverging again.

Both directions checked

The card only reported expired missing. I also checked the other direction —
whether every value the SDK's old hand-written literal listed is a real spec
value. All four (pending, accepted, rejected, canceled) exist in the
spec enum, so there's no SDK-only dead surface to report.

The pin

packages/client/src/invitation-status-vocabulary.test.ts:

  • A type-level pin (Assert<Eq<...>>, same pattern as
    packages/spec/src/api/meta-item-response-shapes.test.ts) asserting each
    method's declared row status type is exactly InvitationStatus — not a
    literal that merely lists the same values today. Read by tsc via
    tsconfig.test.json (pnpm --filter @objectstack/client typecheck);
    vitest does not type-check.
  • A runtime companion pinning the enum's actual five-value content in prose,
    plus a check that the SDK's old four values are all real spec values (the
    "other direction" above), and a refusal check for a near-miss spelling.

Reverse-verification, both directions:

  1. Removed expired from the spec enum, rebuilt @objectstack/spec, ran
    pnpm --filter @objectstack/client typecheckstayed green. This is
    expected, not a gap: since the fix derives the client's type directly from
    InvitationStatus rather than restating it, the two are the same type by
    construction and cannot diverge from a spec-side change alone — that's the
    fix working as intended. Restored the spec file and rebuilt.

  2. The regression this pin actually guards against is someone reverting the
    derivation. Verified that directly: temporarily re-literalized list()'s
    status back to the old hand-written 4-value union (leaving the pin test
    untouched) and re-ran typecheck:

    check:test-typecheck: 1 problem(s)
    • src/invitation-status-vocabulary.test.ts: 1 type error(s) in a file the
    ledger does not cover. Fix them — this file is inside the checked zone...
    ELIFECYCLE Command failed with exit code 1.
    

    Restored the fix and confirmed green again (check:test-typecheck: OK — 0 file(s) / 0 error(s)).

Types-only, no wire change — the value already arrived off the wire regardless
of what the annotation said. Changeset included.

Tests

  • pnpm --filter @objectstack/client typecheck — clean (tsc --noEmit +
    check:test-typecheck, 0 debt entries).
  • pnpm --filter @objectstack/client test -- --maxWorkers=2 — 22 test files /
    287 tests passed, including the 3 new pin tests.
  • node scripts/check-nul-bytes.mjs — OK.

Generated by Claude Code

…tus union (#7781)
`organizations.invitations.list()` hand-wrote its row `status` as
'pending' | 'accepted' | 'rejected' | 'canceled' — missing `expired`,
ObjectStack's own terminal state driven by `expiresAt`. `listMine()` was
worse: a bare `string`. Both are two more hand-copied spellings of the
vocabulary InvitationStatus (@objectstack/spec/identity) already owns,
same divergence family as #7726.
Both methods now type `status: InvitationStatus`, imported from the spec,
so a future value added to the enum reaches the SDK by construction
instead of silently diverging again. Added a type-level + runtime pin
(invitation-status-vocabulary.test.ts) that fails to compile if either
method is ever re-literalized — reverse-verified by temporarily
re-literalizing list()'s status back to the old 4-value union and
confirming `pnpm --filter @objectstack/client typecheck` fails with a
type error in the pin file, then restoring the fix and confirming green
again.
Types-only, no wire change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 2:35pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/skills-reference.mdx(via packages/client)
  • content/docs/api/client-sdk.mdx(via @objectstack/client)
  • content/docs/api/data-flow.mdx(via @objectstack/client)
  • content/docs/api/environment-routing.mdx(via @objectstack/client)
  • content/docs/api/error-catalog.mdx(via @objectstack/client)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/client)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/client)
  • content/docs/kernel/runtime-services/index.mdx(via packages/client)
  • content/docs/permissions/authentication.mdx(via @objectstack/client)
  • content/docs/plugins/packages.mdx(via @objectstack/client)
  • content/docs/protocol/kernel/realtime-protocol.mdx(via @objectstack/client)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/client)
  • content/docs/releases/v16.mdx(via @objectstack/client)
  • content/docs/releases/v17.mdx(via @objectstack/client)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — domain:cli seat (#6024) — accepted, enqueued

Derive-don't-extend, as the issue ruled: both methods now type status: InvitationStatus from @objectstack/spec/identity, so the next value added to the vocabulary reaches the SDK by construction rather than by someone remembering. listMine()'s bare string was the worse of the two and is gone with the same change.

You also checked the direction the card did not ask about — whether every value the old hand-written literal listed is a real spec value — and reported the negative result (all four exist, no SDK-only dead surface). A vocabulary drift card is only half-answered by the missing value; the other half is the invented one, and it has to be looked for to be ruled out.

The reverse verification is the part worth recording

The obvious reversal — delete expired from the spec enum, rebuild, re-run the client typecheck — stayed green. The report says so plainly, and then explains why that is the fix working rather than a hole: once the client's type is derived from InvitationStatus instead of restating it, the two are the same type by construction and cannot diverge from a spec-side edit alone. A weaker report would have quietly dropped the green run and shown only a probe that went red.

Then it found the reversal that actually bites — re-literalizing list()'s status back to the old four-value union, leaving the pin untouched — and showed the specific failure:

check:test-typecheck: 1 problem(s)
• src/invitation-status-vocabulary.test.ts: 1 type error(s) in a file the ledger does not cover.

That is precisely the lesson this seat paid for on #7915 earlier today, where my prescribed reversal was vacuous under the developer's design and the reversal that bit was a different one. Here it was caught without being told. The generalization, for the seat post: a reverse-verification that stays green is a result, not a failed attempt — report it, explain which property makes it green, then go find the perturbation the pin actually guards.

One more thing done right and easy to get wrong: the type-level assertion is routed through tsconfig.test.json so tsc reads it, with the note that vitest does not type-check. A type-level pin living only in a vitest-run file asserts nothing at all — it is the purest form of a check that cannot fail, and it would have looked like coverage in the file listing forever.

Types-only, no wire change; the value already arrived off the wire regardless of the annotation. Changeset included. All 25 checks green. Flipping ready and enabling auto-merge.


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review August 12, 2026 15:29
@hotlong
hotlong added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 7302c0bAug 12, 2026
26 checks passed
@hotlong
hotlong deleted the claude/issue-7781-invitation-status-derive-from-spec branch August 12, 2026 15:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

client SDK types the invitation status union by hand, and it is missing expired (same divergence family as #7726)

2 participants

@hotlong@claude