collapse the dual-world type layer and delete the widening shims

The types/bsky post/profile/starterPack unions drop their @atproto/api arms,
and dangerousIsType/validate go with the old-world guards they wrapped. The
moderation subjects.ts widening shim and rich-text-helpers' asSdkFacets both
existed only to bridge branded and unbranded views, so their 55 and 14 callers
now go straight to @bsky.app/sdk/moderation and the raw facets.

Boundary fallout: lexicon token defs are camelCase schema objects needing
.value, and the branded string slots that the widening used to absorb are now
cast or branded at their producers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-04 04:53:44 +03:00
parent e9a67f2dea
commit ae0750099f
114 changed files with 591 additions and 899 deletions
+7 -2
View File
@@ -15,7 +15,12 @@ import {type FeedAPI, type FeedAPIResponse} from './types'
// we use this fallback marker post to drive this instead. see Feed.tsx
// for the usage.
// -prf
export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = {
/*
* A synthetic marker, not a real view: its `uri`/`did`/`indexedAt` are
* deliberately not well-formed, so the literal is asserted rather than branded.
* Only `post.uri` is ever read (see Feed.tsx).
*/
export const FALLBACK_MARKER_POST = {
post: {
uri: 'fallback-marker-post',
cid: 'fake',
@@ -26,7 +31,7 @@ export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = {
},
indexedAt: new Date().toISOString(),
},
}
} as unknown as app.bsky.feed.defs.FeedViewPost
export class HomeFeedAPI implements FeedAPI {
client: Client
+7 -5
View File
@@ -1,11 +1,13 @@
import {toDatetimeString} from '@atproto/syntax'
import {subDays, subMinutes} from 'date-fns'
import {app} from '#/lexicons'
import {type app} from '#/lexicons'
const DID = `did:plc:z72i7hdynmk6r22z27h6tvur`
const NOW = new Date()
const POST_1_DATE = subMinutes(NOW, 2).toISOString()
const POST_2_DATE = subMinutes(NOW, 4).toISOString()
const POST_3_DATE = subMinutes(NOW, 5).toISOString()
const POST_1_DATE = toDatetimeString(subMinutes(NOW, 2))
const POST_2_DATE = toDatetimeString(subMinutes(NOW, 4))
const POST_3_DATE = toDatetimeString(subMinutes(NOW, 5))
export const DEMO_FEED = {
feed: [
@@ -31,7 +33,7 @@ export const DEMO_FEED = {
issuer: DID,
uri: `at://${DID}/app.bsky.graph.verification/post1`,
isValid: true,
createdAt: subDays(NOW, 11).toISOString(),
createdAt: toDatetimeString(subDays(NOW, 11)),
},
],
verifiedStatus: 'valid',
-125
View File
@@ -1,125 +0,0 @@
import {
hasMutedWord as sdkHasMutedWord,
moderateFeedGenerator as sdkModerateFeedGenerator,
moderateNotification as sdkModerateNotification,
moderatePost as sdkModeratePost,
moderateProfile as sdkModerateProfile,
moderateStatus as sdkModerateStatus,
moderateUserList as sdkModerateUserList,
type ModerationDecision,
type ModerationOpts,
} from '@bsky.app/sdk/moderation'
import {app, chat} from '#/lexicons'
/*
* TRANSITIONAL. The moderation implementation now comes from
* `@bsky.app/sdk/moderation`, whose subject types are the generated
* `#/lexicons` views - so their `did`/`uri`/`cid` fields are branded
* (`DidString`, `AtUriString`). Many read paths still emit the identically
* shaped `@atproto/api` views, whose same fields are plain `string`.
*
* A plain `string` is not assignable to a branded template-literal type, so
* every `moderate*` call taking an unmigrated view fails to typecheck even
* though the value is byte-identical - the runtime only ever reads `.did`,
* `.labels` and `.viewer`, none of which the brand affects.
*
* These wrappers widen each subject parameter to accept a view from either
* world and drop the brand on the way in. Delete this module once every
* producer emits `#/lexicons` views (the `@atproto/api` removal pass) and point
* callers back at `@bsky.app/sdk/moderation` directly.
*/
type AnyProfileSubject =
| app.bsky.actor.defs.ProfileViewBasic
| app.bsky.actor.defs.ProfileView
| app.bsky.actor.defs.ProfileViewDetailed
| chat.bsky.actor.defs.ProfileViewBasic
| app.bsky.actor.defs.ProfileViewBasic
| app.bsky.actor.defs.ProfileView
| app.bsky.actor.defs.ProfileViewDetailed
| chat.bsky.actor.defs.ProfileViewBasic
type AnyPostSubject = app.bsky.feed.defs.PostView | app.bsky.feed.defs.PostView
type AnyUserListSubject =
| app.bsky.graph.defs.ListViewBasic
| app.bsky.graph.defs.ListView
| app.bsky.graph.defs.ListViewBasic
| app.bsky.graph.defs.ListView
type AnyFeedGeneratorSubject =
| app.bsky.feed.defs.GeneratorView
| app.bsky.feed.defs.GeneratorView
type AnyNotificationSubject =
| app.bsky.notification.listNotifications.Notification
| app.bsky.notification.listNotifications.Notification
export function moderateProfile(
subject: AnyProfileSubject,
opts: ModerationOpts,
): ModerationDecision {
return sdkModerateProfile(subject as app.bsky.actor.defs.ProfileView, opts)
}
export function moderateStatus(
subject: AnyProfileSubject,
opts: ModerationOpts,
): ModerationDecision {
return sdkModerateStatus(subject as app.bsky.actor.defs.ProfileView, opts)
}
export function moderatePost(
subject: AnyPostSubject,
opts: ModerationOpts,
): ModerationDecision {
return sdkModeratePost(subject as app.bsky.feed.defs.PostView, opts)
}
export function moderateUserList(
subject: AnyUserListSubject,
opts: ModerationOpts,
): ModerationDecision {
return sdkModerateUserList(subject as app.bsky.graph.defs.ListView, opts)
}
export function moderateFeedGenerator(
subject: AnyFeedGeneratorSubject,
opts: ModerationOpts,
): ModerationDecision {
return sdkModerateFeedGenerator(
subject as app.bsky.feed.defs.GeneratorView,
opts,
)
}
export function moderateNotification(
subject: AnyNotificationSubject,
opts: ModerationOpts,
): ModerationDecision {
return sdkModerateNotification(
subject as app.bsky.notification.listNotifications.Notification,
opts,
)
}
/**
* Widens `facets`/`actor` for the same reason the `moderate*` wrappers widen
* their subjects: mute-word matching reads only `text`/`features`/`langs`, none
* of which the brand affects.
*/
export function hasMutedWord(params: {
mutedWords: app.bsky.actor.defs.MutedWord[]
text: string
facets?: app.bsky.richtext.facet.Main[] | app.bsky.richtext.facet.Main[]
outlineTags?: string[]
languages?: string[]
actor?: AnyProfileSubject
}): boolean {
return sdkHasMutedWord({
...params,
facets: params.facets as app.bsky.richtext.facet.Main[] | undefined,
actor: params.actor as app.bsky.actor.defs.ProfileView | undefined,
})
}
-23
View File
@@ -30,26 +30,3 @@ export function richTextToString(rt: RichText, loose: boolean): string {
return result
}
/**
* Widens facets typed by the legacy `@atproto/api` codegen into the shape the
* SDK's `RichText` accepts.
*
* The two are the same lexicon and identical at runtime; they differ only in
* that the SDK brands `did`/`uri` as template literal types, which makes the
* legacy `string` versions unassignable. Call this where facets read off an
* `@atproto/api` view type are handed to `new RichText(...)`.
*
* Transitional: it goes away once the view types come from the SDK too.
*/
export function asSdkFacets(
facets: {index: {byteStart: number; byteEnd: number}; features: unknown[]}[],
): app.bsky.richtext.facet.Main[]
export function asSdkFacets(
facets:
| {index: {byteStart: number; byteEnd: number}; features: unknown[]}[]
| undefined,
): app.bsky.richtext.facet.Main[] | undefined
export function asSdkFacets(facets: unknown) {
return facets as app.bsky.richtext.facet.Main[] | undefined
}
+14
View File
@@ -1,6 +1,7 @@
import {
getMain,
type InferMethodError,
LexError,
type Main,
type Procedure,
type Query,
@@ -27,6 +28,19 @@ export function getErrorStatus(e: unknown): number | undefined {
return e instanceof XrpcResponseError ? e.status : undefined
}
/**
* The lexicon error code (`err.error`), or undefined when `e` carries none.
* Gated on `LexError` (the base of every `XrpcError`) rather than `XrpcError`,
* so the non-XRPC lex errors are covered too.
*
* Prefer {@link matchXrpcError} where the method that threw is known: it
* constrains the compared name to that method's declared errors. Use this only
* where the source method is genuinely ambiguous.
*/
export function getErrorName(e: unknown): string | undefined {
return e instanceof LexError ? e.error : undefined
}
/**
* Same nsid means `e` was thrown for this method schema, so `e` can be
* treated as an `XrpcResponseError<M>` - which is what lets the SDK's