@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.
- package/README.md +85 -10
- package/dist/client.d.ts +5 -0
- package/dist/client.js +7 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.js +11 -1
- package/dist/resources/agents.d.ts +48 -0
- package/dist/resources/agents.js +22 -0
- package/dist/resources/curator.d.ts +11 -4
- package/dist/resources/discovery-fallover.d.ts +8 -0
- package/dist/resources/discovery-fallover.js +26 -0
- package/dist/resources/discovery-first-screen.d.ts +30 -0
- package/dist/resources/discovery-first-screen.js +149 -0
- package/dist/resources/discovery-merge.d.ts +144 -0
- package/dist/resources/discovery-merge.js +479 -0
- package/dist/resources/discovery-progressive.d.ts +117 -0
- package/dist/resources/discovery-progressive.js +414 -0
- package/dist/resources/discovery-safety.d.ts +32 -0
- package/dist/resources/discovery-safety.js +521 -0
- package/dist/resources/discovery.d.ts +59 -16
- package/dist/resources/discovery.js +147 -47
- package/dist/resources/feeds.d.ts +3 -1
- package/dist/resources/judgments.d.ts +252 -0
- package/dist/resources/judgments.js +79 -0
- package/dist/resources/notifications.d.ts +7 -2
- package/dist/resources/protocols.d.ts +204 -17
- package/dist/resources/protocols.js +24 -5
- package/dist/resources/search.d.ts +9 -1
- package/dist/resources/settings.d.ts +8 -1
- package/dist/resources/settings.js +9 -0
- package/dist/resources/signals.d.ts +58 -0
- package/dist/resources/signals.js +32 -0
- package/dist/types.d.ts +67 -0
- package/llm.md +24 -7
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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. `
|
|
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
|