@recursiv/sdk 0.7.70 → 0.7.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import { ConflictError } from '../errors.js';
3
+ import type { SingleResponse } from '../types.js';
4
+ /** A Minds post (`post`, its uuid) or an open-network post (`network_post`, its subject id). */
5
+ export type JudgmentSubjectKind = 'post' | 'network_post';
6
+ /** Who wrote the label: Jev (`model`), or staff who overrode it (`review`). */
7
+ export type JudgmentSource = 'model' | 'review';
8
+ /** Whether the server's judgment store answered. `'unavailable'`: only the category index's label is known; hide label actions. */
9
+ export type PostLabelsStatus = 'ok' | 'unavailable';
10
+ /** How an override met its quorum: one reviewer (a safety appeal), two agreeing reviewers, or one beside an agreeing Jev re-judge. */
11
+ export type JudgmentOverrideMethod = 'reviewer' | 'two_reviewers' | 'reviewer_and_rejudge';
12
+ export interface PostLabelsQuery {
13
+ kind: JudgmentSubjectKind;
14
+ /** A Minds post id, or an open-network post's subject id. */
15
+ id: string;
16
+ }
17
+ export interface PostLabelsSubject {
18
+ kind: JudgmentSubjectKind;
19
+ id: string;
20
+ }
21
+ export interface PostLabelsCategory {
22
+ id: string;
23
+ name: string;
24
+ }
25
+ /** The compass band in words. Never a probability or a cell code. */
26
+ export interface PostLabelsPolitics {
27
+ /** The judgment a member disputes with `dispute(judgment_id, { question_key: 'placement', proposed })`. */
28
+ judgment_id: string;
29
+ politics: 'Takes a side' | 'Maybe' | 'No';
30
+ /** Each placed axis in words, e.g. `{ axis: 'Liberty ↔ Order', band: 'Order' }`. Empty unless the post takes a side. */
31
+ axes: Array<{
32
+ axis: string;
33
+ band: string;
34
+ }>;
35
+ model: string;
36
+ rubric: string;
37
+ source: JudgmentSource;
38
+ judged_at: string | null;
39
+ }
40
+ /** One stored judgment, as the post's author and staff see it. */
41
+ export interface PostJudgmentRecord {
42
+ id: string;
43
+ rubric: string;
44
+ model: string;
45
+ source: JudgmentSource;
46
+ judged_at: string | null;
47
+ /** The typed answers, every probability included. */
48
+ answers: Record<string, unknown>;
49
+ political: number | null;
50
+ cell: string | null;
51
+ reason: 'hate' | 'spam' | null;
52
+ }
53
+ /** A rubric's current judgment in the full view. */
54
+ export interface PostJudgment extends PostJudgmentRecord {
55
+ /** How many staff reviewers wrote it (0 for Jev's own). */
56
+ reviewers: number;
57
+ /** Staff only. */
58
+ reviewer_ids?: string[];
59
+ /** Staff only. */
60
+ review_note?: string | null;
61
+ /** Whether `dispute()` takes it: political-compass and safety labels (safety: the author only). */
62
+ disputable: boolean;
63
+ /** Jev's judgment a staff review replaced, for the same words. */
64
+ superseded: PostJudgmentRecord | null;
65
+ }
66
+ /** The safety verdict the Discover floor applies to the post now, with who judged it (full view, Minds posts). */
67
+ export type PostSafetyAttribution = {
68
+ status: 'judged';
69
+ reason: 'hate' | 'spam' | null;
70
+ unsafe: boolean;
71
+ hate: number;
72
+ spam: number;
73
+ rubric: string;
74
+ /** The judging model, `'review'` for a staff review; null when only the cache holds the verdict. */
75
+ model: string | null;
76
+ source: JudgmentSource | 'cache';
77
+ judgedAt: string | null;
78
+ /** A staff review the floor does not apply yet (the cache still holds another verdict). */
79
+ unappliedReview?: {
80
+ reason: 'hate' | 'spam' | null;
81
+ unsafe: boolean;
82
+ hate: number;
83
+ spam: number;
84
+ judgedAt: string;
85
+ };
86
+ } | {
87
+ status: 'pending';
88
+ } | {
89
+ status: 'not_judgeable';
90
+ };
91
+ /** A visitor's view (`getPublic`): the category only. */
92
+ export interface PostLabelsCategoryView {
93
+ subject: PostLabelsSubject;
94
+ view: 'category';
95
+ category: PostLabelsCategory | null;
96
+ status: PostLabelsStatus;
97
+ }
98
+ /** Any other member's view: the category and the compass band in words, with the id to dispute it. */
99
+ export interface PostLabelsSummaryView {
100
+ subject: PostLabelsSubject;
101
+ view: 'summary';
102
+ category: PostLabelsCategory | null;
103
+ politics: PostLabelsPolitics | null;
104
+ status: PostLabelsStatus;
105
+ }
106
+ /** The post's author's and staff's view: every rubric's current judgment, the safety verdict and Viral. */
107
+ export interface PostLabelsFullView {
108
+ subject: PostLabelsSubject;
109
+ view: 'full';
110
+ category: PostLabelsCategory | null;
111
+ politics: PostLabelsPolitics | null;
112
+ judgments: PostJudgment[];
113
+ /** null for an open-network post. */
114
+ safety: PostSafetyAttribution | null;
115
+ /** Why the post is withheld from Viral ("Withheld from Viral: <reason>"), or null. */
116
+ viral: {
117
+ withheld: string | null;
118
+ };
119
+ status: PostLabelsStatus;
120
+ }
121
+ /** One post's labels, in the view the caller may see: switch on `view`. */
122
+ export type PostLabels = PostLabelsFullView | PostLabelsSummaryView | PostLabelsCategoryView;
123
+ export interface DisputeJudgmentInput {
124
+ /** The label's question: `'placement'` for a political-compass label, `'safety'` for a safety label (its author only). */
125
+ question_key: string;
126
+ /** The answer it should be: a compass cell (`'lo'`, `'cm'`, …) or `'not_political'`; for safety `'none'`, `'hate'` or `'spam'`. */
127
+ proposed: string;
128
+ /** At most 500 characters. */
129
+ note?: string;
130
+ }
131
+ /** A filed dispute. The author's is an appeal and goes to the moderation queue; anyone else's feeds the evaluation. */
132
+ export interface JudgmentDispute {
133
+ id: string;
134
+ judgment_id: string;
135
+ /** True when the caller wrote the post: an appeal. */
136
+ appeal: boolean;
137
+ /** Whether it waits in the moderation queue now. */
138
+ queued: boolean;
139
+ /** Present on a repeat: the member's first dispute of this judgment, unchanged. */
140
+ existing?: true;
141
+ }
142
+ export type ReviewJudgmentInput = {
143
+ outcome: 'uphold';
144
+ note?: string;
145
+ } | {
146
+ outcome: 'override';
147
+ /** The answer the label becomes. */
148
+ proposed: string;
149
+ /** At most 2,000 characters. */
150
+ note?: string;
151
+ /** A safety override to `'none'` that frees other posts with exactly these words: send true after a 409 `shared_words`. */
152
+ apply_to_shared?: boolean;
153
+ };
154
+ /** 200: the label stands, and its queue items are closed. */
155
+ export interface JudgmentReviewUpheld {
156
+ status: 'upheld';
157
+ resolved: number;
158
+ }
159
+ /** 202: this reviewer's vote is recorded; a compass label needs another reviewer (or an agreeing Jev re-judge). */
160
+ export interface JudgmentReviewAwaitingReviewer {
161
+ status: 'awaiting_reviewer';
162
+ vote_id: string;
163
+ agreeing: number;
164
+ needed: number;
165
+ }
166
+ /** 200: the override is written and in the public moderation log. */
167
+ export interface JudgmentReviewOverridden {
168
+ status: 'overridden';
169
+ review_id: string;
170
+ moderation_action_id: string;
171
+ method: JudgmentOverrideMethod;
172
+ reviewers: number;
173
+ resolved: number;
174
+ trust_log: 'appended' | 'disabled' | 'duplicate';
175
+ /** A safety review: whether the Discover floor applies it now; null for a compass label. */
176
+ applied_to_discover: boolean | null;
177
+ /** A safety review of words the post no longer has: recorded, never applied. */
178
+ words_changed: boolean | null;
179
+ shared_posts: number | null;
180
+ }
181
+ export type JudgmentReview = JudgmentReviewUpheld | JudgmentReviewAwaitingReviewer | JudgmentReviewOverridden;
182
+ /** The `details` of a 409 `shared_words` from `review()`. */
183
+ export interface JudgmentSharedWordsDetails {
184
+ shared_posts: number;
185
+ }
186
+ /** The 409 `review()` throws before a safety override frees other posts with exactly these words. */
187
+ export type JudgmentSharedWordsError = ConflictError & {
188
+ code: 'shared_words';
189
+ details: JudgmentSharedWordsDetails;
190
+ };
191
+ /**
192
+ * Whether `error` is the 409 `shared_words` of `review()`: nothing was recorded, and
193
+ * `error.details.shared_posts` other posts have exactly these words. Resend with
194
+ * `apply_to_shared: true` once the reviewer agrees to change them all.
195
+ */
196
+ export declare function isJudgmentSharedWordsError(error: unknown): error is JudgmentSharedWordsError;
197
+ /**
198
+ * Jev's labels of one post: read them, dispute or appeal one, and (staff)
199
+ * decide a dispute.
200
+ *
201
+ * Budgets: 60 label reads per member an hour (25 of one author's posts) and
202
+ * 20 disputes a day. Past one, the server answers 429 with
203
+ * `X-RateLimit-Scope: action`: the SDK throws `RateLimitError` at once
204
+ * (`scope === 'action'`, `resetAt`, a member-facing `message`) and never
205
+ * retries it. Disputes and reviews answer 503 until the server's judgment
206
+ * store is applied; reads answer `status: 'unavailable'` instead.
207
+ */
208
+ export declare class JudgmentsResource {
209
+ private client;
210
+ constructor(client: HttpClient);
211
+ /**
212
+ * One post's labels as the caller may see them. The post's author and staff
213
+ * get `view: 'full'` (every judgment with its answers, the safety verdict,
214
+ * Viral); any other member who can see the post gets `view: 'summary'` (the
215
+ * category and the compass band in words). `status: 'unavailable'` is
216
+ * returned, not thrown. A post the caller cannot see throws `NotFoundError`.
217
+ * Open-network posts need a key bound to an app (400 `app_required`).
218
+ *
219
+ * @example
220
+ * const { data } = await r.judgments.get({ kind: 'post', id: postId });
221
+ * if (data.status === 'ok' && data.view !== 'category' && data.politics) showBand(data.politics);
222
+ */
223
+ get(query: PostLabelsQuery): Promise<SingleResponse<PostLabels>>;
224
+ /**
225
+ * A signed-out visitor's view of one post's labels: the category only.
226
+ * Keyless: use an anonymous client (`new Recursiv({ anonymous: true })`).
227
+ */
228
+ getPublic(projectId: string, query: PostLabelsQuery): Promise<SingleResponse<PostLabelsCategoryView>>;
229
+ /**
230
+ * Disagree with a label. The post's author's dispute is an appeal and goes
231
+ * to the moderation queue; anyone else's feeds the evaluation and changes
232
+ * nothing on its own. One per member and judgment: a repeat returns the
233
+ * first with `existing: true`. Sent once, never retried automatically.
234
+ *
235
+ * @example
236
+ * await r.judgments.dispute(labels.politics.judgment_id, { question_key: 'placement', proposed: 'not_political' });
237
+ */
238
+ dispute(judgmentId: string, input: DisputeJudgmentInput): Promise<SingleResponse<JudgmentDispute>>;
239
+ /**
240
+ * Staff: uphold a disputed label, or vote to override it. A compass label
241
+ * changes when two reviewers propose the same answer (or one, beside an
242
+ * agreeing Jev re-judge): the first gets `status: 'awaiting_reviewer'`
243
+ * (HTTP 202). Every override is in the public moderation log.
244
+ *
245
+ * Sent once, never retried: an override that met its quorum but lost its
246
+ * response would otherwise be recorded again as a fresh vote. A safety
247
+ * override to `'none'` that would free other posts with exactly these words
248
+ * throws a 409 (`isJudgmentSharedWordsError`, `details.shared_posts`) until
249
+ * resent with `apply_to_shared: true`.
250
+ */
251
+ review(judgmentId: string, input: ReviewJudgmentInput): Promise<SingleResponse<JudgmentReview>>;
252
+ }
@@ -0,0 +1,79 @@
1
+ import { ConflictError } from '../errors.js';
2
+ /**
3
+ * Whether `error` is the 409 `shared_words` of `review()`: nothing was recorded, and
4
+ * `error.details.shared_posts` other posts have exactly these words. Resend with
5
+ * `apply_to_shared: true` once the reviewer agrees to change them all.
6
+ */
7
+ export function isJudgmentSharedWordsError(error) {
8
+ if (!(error instanceof ConflictError) || error.code !== 'shared_words')
9
+ return false;
10
+ const count = error.details?.shared_posts;
11
+ return typeof count === 'number' && Number.isFinite(count);
12
+ }
13
+ const path = (judgmentId, action) => `/content-judgments/${encodeURIComponent(judgmentId)}/${action}`;
14
+ /**
15
+ * Jev's labels of one post: read them, dispute or appeal one, and (staff)
16
+ * decide a dispute.
17
+ *
18
+ * Budgets: 60 label reads per member an hour (25 of one author's posts) and
19
+ * 20 disputes a day. Past one, the server answers 429 with
20
+ * `X-RateLimit-Scope: action`: the SDK throws `RateLimitError` at once
21
+ * (`scope === 'action'`, `resetAt`, a member-facing `message`) and never
22
+ * retries it. Disputes and reviews answer 503 until the server's judgment
23
+ * store is applied; reads answer `status: 'unavailable'` instead.
24
+ */
25
+ export class JudgmentsResource {
26
+ client;
27
+ constructor(client) {
28
+ this.client = client;
29
+ }
30
+ /**
31
+ * One post's labels as the caller may see them. The post's author and staff
32
+ * get `view: 'full'` (every judgment with its answers, the safety verdict,
33
+ * Viral); any other member who can see the post gets `view: 'summary'` (the
34
+ * category and the compass band in words). `status: 'unavailable'` is
35
+ * returned, not thrown. A post the caller cannot see throws `NotFoundError`.
36
+ * Open-network posts need a key bound to an app (400 `app_required`).
37
+ *
38
+ * @example
39
+ * const { data } = await r.judgments.get({ kind: 'post', id: postId });
40
+ * if (data.status === 'ok' && data.view !== 'category' && data.politics) showBand(data.politics);
41
+ */
42
+ get(query) {
43
+ return this.client.get('/content-judgments', { kind: query.kind, id: query.id });
44
+ }
45
+ /**
46
+ * A signed-out visitor's view of one post's labels: the category only.
47
+ * Keyless: use an anonymous client (`new Recursiv({ anonymous: true })`).
48
+ */
49
+ getPublic(projectId, query) {
50
+ return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/content-judgments`, { kind: query.kind, id: query.id });
51
+ }
52
+ /**
53
+ * Disagree with a label. The post's author's dispute is an appeal and goes
54
+ * to the moderation queue; anyone else's feeds the evaluation and changes
55
+ * nothing on its own. One per member and judgment: a repeat returns the
56
+ * first with `existing: true`. Sent once, never retried automatically.
57
+ *
58
+ * @example
59
+ * await r.judgments.dispute(labels.politics.judgment_id, { question_key: 'placement', proposed: 'not_political' });
60
+ */
61
+ dispute(judgmentId, input) {
62
+ return this.client.post(path(judgmentId, 'disputes'), input, { retry: false });
63
+ }
64
+ /**
65
+ * Staff: uphold a disputed label, or vote to override it. A compass label
66
+ * changes when two reviewers propose the same answer (or one, beside an
67
+ * agreeing Jev re-judge): the first gets `status: 'awaiting_reviewer'`
68
+ * (HTTP 202). Every override is in the public moderation log.
69
+ *
70
+ * Sent once, never retried: an override that met its quorum but lost its
71
+ * response would otherwise be recorded again as a fresh vote. A safety
72
+ * override to `'none'` that would free other posts with exactly these words
73
+ * throws a 409 (`isJudgmentSharedWordsError`, `details.shared_posts`) until
74
+ * resent with `apply_to_shared: true`.
75
+ */
76
+ review(judgmentId, input) {
77
+ return this.client.post(path(judgmentId, 'reviews'), input, { retry: false });
78
+ }
79
+ }
@@ -91,7 +91,11 @@ export interface Notification {
91
91
  actor?: NotificationActor | null;
92
92
  post_preview?: NotificationPostPreview | null;
93
93
  }
94
- export type NotificationCategory = 'all' | 'social' | 'system';
94
+ /**
95
+ * `mentions` (SDK 0.7.72) is a Mentions tab: mentions and replies only, a
96
+ * subset of `social`. An older server answers it 400.
97
+ */
98
+ export type NotificationCategory = 'all' | 'social' | 'system' | 'mentions';
95
99
  export type NotificationScopeParams = {
96
100
  /** Which kind of notification to count. Defaults to `all`. Same meaning as in `list`. */
97
101
  category?: NotificationCategory;
@@ -114,7 +118,8 @@ export interface ListNotificationsParams extends PaginationParams {
114
118
  * reposts, mentions, community invites, joins and join requests, moderation
115
119
  * decisions). `system` is everything operational (agent, task, deploy,
116
120
  * billing, security). Direct messages are in neither: they live in the chat
117
- * inbox. `all` (the default) lists every row that has not been archived.
121
+ * inbox. `mentions` is the posts that name you or answer you (mentions and
122
+ * replies). `all` (the default) lists every row that has not been archived.
118
123
  *
119
124
  * Ask for it here rather than filtering the page you get back: the server
120
125
  * applies the limit AFTER the category, so a client that filters afterwards