Repository navigation
client SDK types the invitation status union by hand, and it is missing expired (same divergence family as #7726) #7781
Description
Activity
Lane re-route:
domain:spec→domain:cli(spec-lane PM, sessionsession_01JY2Q5Xto1u8YHADgrZDTnk, under the standing lane-triage authorization granted 2026-08-11 「你的车道你可以执行 分类改标」).Rationale: the divergent artifact is the client SDK's hand-typed status union — the fix is the SDK importing/deriving from the spec's
InvitationStatusrather than restating it, which is client-package surface. The spec side is already settled: #7726 (PR #7782, ACCEPT-reviewed, in the landing relay) widened the enum to the five shipped values and boundsys_invitation's options to it with a parity test. This card is the same divergence family's third spelling; whoever picks it up should apply the same bind-don't-restate shape, and note the union must include bothexpired(missing today) andcanceled(added by #7726).
Generated by Claude Code
Findings-round promotion:
pm:queue(domain:clikept, type Bug).The blocking authority landed and the divergence widened:
packages/spec/src/organization.zod.ts:127is nowz.enum(['pending','accepted','rejected','expired','canceled'])(via #7726 / PR #7782), whilepackages/client/src/index.ts:2030still hand-writes a 3-value union — now missing two values, not one.listMineat:2052still typesstatus: string.Dispatch note: apply the bind-don't-restate shape the spec-lane PM already indicated — derive the client union from the spec enum rather than restating literals, so the next enum addition cannot re-open this divergence. Small diff, one file.
本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。
Generated by Claude Code
Claimed by the
domain:cliPM seat (#6024, sessionsession_01B3Kurx8qufrDzNjk4rag7V).
Branch:claude/issue-7781-invitation-status-derive-from-spec· dispatch:mode:subagent,model: sonnet.Anchor verified:
packages/client/src/index.ts:2030hand-writes
status: 'pending' | 'accepted' | 'rejected' | 'canceled', whileInvitationStatusin
packages/spec/src/identity/organizationis the declared source of truth.Ruling: derive, do not extend
⛔ Do not just add
'expired'to the literal. That fixes today's divergence and leaves the mechanism that produced it — the next value added to the spec diverges again, silently, exactly as this one did. This is the same family as #7726, and this lane has closed five one-rule-two-implementations bugs today; do not add a sixth by hand.Type the field from the spec's own union so a value added there reaches the SDK by construction. If the SDK genuinely cannot import from
@objectstack/specon that path, say so with the reason — that is a finding, and it changes the shape of the fix.⚠️ Check BOTH directions — the card only names oneThe card reports
expiredmissing. A hand-written union can diverge both ways, and the local list also carries'rejected'. Verify against the spec whether every value the SDK lists actually exists there. A value the SDK accepts that the platform never emits is dead surface a caller may be branching on — report it rather than deleting it silently.Pins
- A test that fails if the two ever diverge again — derived from the spec union, ⛔ not a second hand-written list that happens to agree today.
- Reverse-verify: remove a value from the spec union (or add one) and confirm the check fails.
Wire behaviour does not change; this is a types/derivation fix. Say so in the changeset.
Generated by Claude Code
os-dev-report
{ "issue": 7781, "status": "done", "branch": "claude/issue-7781-invitation-status-derive-from-spec", "pr": "https://github.com/objectstack-ai/objectstack/pull/8076", "premise_still_valid": true, "derivation_mechanism": "Both organizations.invitations.list() and listMine() in packages/client/src/index.ts now type `status: InvitationStatus`, imported as `import type { InvitationStatus } from '@objectstack/spec/identity'` — the same union sys_invitation.status binds its select options to (#7726). @objectstack/client already depends on @objectstack/spec and already imports other types from it the same way (e.g. ApprovalStatus from @objectstack/spec/contracts), so no new dependency was needed.", "values_missing_in_sdk": ["expired"], "values_sdk_has_that_spec_lacks": [], "tests_added": "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 list()'s and listMine()'s declared row `status` type is exactly InvitationStatus (read by tsc via tsconfig.test.json, not vitest), plus 3 runtime tests pinning the enum's five-value content, confirming the SDK's old 4 hand-listed values are all real spec values, and a refusal check. pnpm --filter @objectstack/client typecheck: clean (tsc --noEmit + check:test-typecheck, 0 debt). pnpm --filter @objectstack/client test -- --maxWorkers=2: 'Test Files 22 passed (22)', 'Tests 287 passed (287)'.", "reverse_verification": "Two directions tested. (1) Removed 'expired' from the spec enum, rebuilt @objectstack/spec, ran client typecheck — stayed GREEN. This is expected, not a gap: since the client's type is the same type reference as the spec's (derived, not copied), a spec-only change cannot create a divergence for the pin to catch — that is the fix working as designed. Restored spec and rebuilt. (2) The regression the pin actually guards against is reverting the derivation: temporarily re-literalized list()'s status back to the old 'pending'|'accepted'|'rejected'|'canceled' union (pin test untouched) and re-ran typecheck — RED: 'check:test-typecheck: 1 problem(s) • src/invitation-status-vocabulary.test.ts: 1 type error(s)... ELIFECYCLE Command failed with exit code 1'. Restored the fix; typecheck green again ('check:test-typecheck: OK — 0 file(s) / 0 error(s)').", "out_of_scope_findings": [], "risks": [ "The type-level pin only fails to compile if a future re-literalization diverges from InvitationStatus's actual member set; a re-literalization that happens to list the same five values today would still compile (structural equality), same as any Eq-based pin — no different from the precedent in packages/spec/src/api/meta-item-response-shapes.test.ts.", "listMine()'s row shape was widened in type only (status: string -> InvitationStatus); no other fields on that method's row type were touched, so it still doesn't declare teamId the way list() does — left as-is, out of scope for this card." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Observation
Found while implementing #7726 (the spec
InvitationStatusenum was missingcanceled). The same vocabulary has a third hand-written copy, in the client SDK, and that one diverges in the opposite direction.packages/client/src/index.ts:2030— the row typeorganizations.invitations.list()casts its response to:Four values, and the missing one is
expired. That is not a better-auth artefact:expiredis ObjectStack's own terminal state, driven byexpiresAt, declared onsys_invitation.status, and filtered on by the object's "Expired / Canceled" listView. Solist()can return a row whosestatusthe declared type says is impossible.After #7726 the vocabulary has one authority — the spec's
InvitationStatus— and the platform object binds its select options to it. This literal does not, and it is the remaining unbound copy.Note the neighbouring method is inconsistent with it too:
listMine()(:2052) types the same field as a barestring.Impact
Type-level only, and low: TypeScript narrowing over the union would not offer an
expiredbranch, and aswitchwritten against it would silently omit that case. No runtime behaviour depends on the annotation — it is a cast over a parsed JSON response, so the value arrives regardless. Nothing in this repo consumes the field off that call today.Filed as an observation rather than a defect: nobody hits it at runtime, but it is a declared-vs-shipped divergence of exactly the kind #7726 was, so it is worth closing deliberately instead of leaving for the next reader to rediscover.
Suggested direction (not a ruling)
Type both methods from the spec enum rather than by hand, e.g.
status: InvitationStatusimported from@objectstack/spec/identity, which makes this copy track the authority the waysys_invitationnow does. Whether@objectstack/clientshould take that dependency for a response annotation is the open question — it may prefer to stay dependency-light, in which case the honest alternative is a comment naming the enum as the source and a pin comparing the two.Source
Out-of-scope finding from #7726 (PR: spec enum widened to five values, platform object bound to the enum). Not fixed there — different package, different call, and the routing question above is unresolved.