@read-frog/api-contract 0.12.1 → 0.12.3

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.
@@ -1,12 +1,29 @@
1
1
  import { eventIterator, oc } from "@orpc/contract"
2
+ import { pickPublicErrorMap } from "#/public-errors"
2
3
  import {
3
4
  HostedAiNoteSuggestionStreamInputSchema,
5
+ HostedAiStatusInputSchema,
6
+ HostedAiStatusOutputSchema,
4
7
  HostedAiStreamPartSchema,
5
8
  HostedAiStreamStructuredObjectInputSchema,
6
9
  HostedAiStreamTextInputSchema,
7
10
  } from "#/schemas/hosted-ai"
8
11
 
12
+ const hostedAiStreamErrors = pickPublicErrorMap(
13
+ "HOSTED_AI_TIER_RESTRICTED",
14
+ "HOSTED_AI_QUOTA_EXHAUSTED",
15
+ )
16
+
9
17
  export const hostedAiContract = {
18
+ status: oc
19
+ .route({
20
+ method: "GET",
21
+ path: "/hosted-ai/status",
22
+ summary: "Get Built-in AI availability and quota status",
23
+ tags: ["Hosted AI"],
24
+ })
25
+ .input(HostedAiStatusInputSchema)
26
+ .output(HostedAiStatusOutputSchema),
10
27
  translate: {
11
28
  streamText: oc
12
29
  .route({
@@ -16,7 +33,8 @@ export const hostedAiContract = {
16
33
  tags: ["Hosted AI"],
17
34
  })
18
35
  .input(HostedAiStreamTextInputSchema)
19
- .output(eventIterator(HostedAiStreamPartSchema)),
36
+ .output(eventIterator(HostedAiStreamPartSchema))
37
+ .errors(hostedAiStreamErrors),
20
38
  },
21
39
  customAction: {
22
40
  streamStructuredObject: oc
@@ -27,7 +45,8 @@ export const hostedAiContract = {
27
45
  tags: ["Hosted AI"],
28
46
  })
29
47
  .input(HostedAiStreamStructuredObjectInputSchema)
30
- .output(eventIterator(HostedAiStreamPartSchema)),
48
+ .output(eventIterator(HostedAiStreamPartSchema))
49
+ .errors(hostedAiStreamErrors),
31
50
  },
32
51
  noteSuggestion: {
33
52
  streamStructuredObject: oc
@@ -38,6 +57,7 @@ export const hostedAiContract = {
38
57
  tags: ["Hosted AI"],
39
58
  })
40
59
  .input(HostedAiNoteSuggestionStreamInputSchema)
41
- .output(eventIterator(HostedAiStreamPartSchema)),
60
+ .output(eventIterator(HostedAiStreamPartSchema))
61
+ .errors(hostedAiStreamErrors),
42
62
  },
43
63
  }
@@ -1,5 +1,7 @@
1
1
  import { pickPublicErrorMap } from "#/public-errors"
2
2
  import {
3
+ srsListCardRevlogsInputSchema,
4
+ srsListCardRevlogsOutputSchema,
3
5
  srsReviewInputSchema,
4
6
  srsReviewOutputSchema,
5
7
  srsRollbackReviewInputSchema,
@@ -25,6 +27,17 @@ export const srsContract = {
25
27
  .input(srsScheduleStatusStatsInputSchema)
26
28
  .output(srsScheduleStatusStatsOutputSchema),
27
29
 
30
+ listCardRevlogs: notebaseProcedure
31
+ .errors(pickPublicErrorMap("CARD_NOT_FOUND"))
32
+ .route({
33
+ method: "GET",
34
+ path: "/srs/cards/{cardId}/revlogs",
35
+ summary: "List complete review history for a card",
36
+ tags: ["SRS"],
37
+ })
38
+ .input(srsListCardRevlogsInputSchema)
39
+ .output(srsListCardRevlogsOutputSchema),
40
+
28
41
  review: notebaseProcedure
29
42
  .errors(pickPublicErrorMap(
30
43
  "CARD_NOT_FOUND",
@@ -0,0 +1,42 @@
1
+ import { pickPublicErrorMap } from "#/public-errors"
2
+ import { notebaseRowGrowthStatsOutputSchema } from "#/schemas/notebase-stats"
3
+ import {
4
+ srsMemoryGrowthStatsOutputSchema,
5
+ } from "#/schemas/srs-stats"
6
+ import { activityStatsOutputSchema, statsInputSchema } from "#/schemas/stats"
7
+ import { notebaseProcedure } from "./shared"
8
+
9
+ export const statsContract = {
10
+ activity: notebaseProcedure
11
+ .errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND"))
12
+ .route({
13
+ method: "GET",
14
+ path: "/stats/activity",
15
+ summary: "Get SRS and notebase activity statistics",
16
+ tags: ["Statistics"],
17
+ })
18
+ .input(statsInputSchema)
19
+ .output(activityStatsOutputSchema),
20
+
21
+ srsMemoryGrowth: notebaseProcedure
22
+ .errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND"))
23
+ .route({
24
+ method: "GET",
25
+ path: "/srs/stats/memory-growth",
26
+ summary: "Get SRS long-term memory growth statistics",
27
+ tags: ["Statistics"],
28
+ })
29
+ .input(statsInputSchema)
30
+ .output(srsMemoryGrowthStatsOutputSchema),
31
+
32
+ notebaseRowGrowth: notebaseProcedure
33
+ .errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND"))
34
+ .route({
35
+ method: "GET",
36
+ path: "/notebases/stats/row-growth",
37
+ summary: "Get notebase row growth statistics",
38
+ tags: ["Statistics"],
39
+ })
40
+ .input(statsInputSchema)
41
+ .output(notebaseRowGrowthStatsOutputSchema),
42
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { notebaseContract } from "./contracts/notebase"
9
9
  import { notebaseColumnContract } from "./contracts/notebase-column"
10
10
  import { notebaseRowContract } from "./contracts/notebase-row"
11
11
  import { srsContract } from "./contracts/srs"
12
+ import { statsContract } from "./contracts/stats"
12
13
  import { userContract } from "./contracts/user"
13
14
  import { videoTranscriptContract } from "./contracts/video-transcript"
14
15
 
@@ -23,6 +24,7 @@ export const contract = {
23
24
  notebaseColumn: notebaseColumnContract,
24
25
  notebaseRow: notebaseRowContract,
25
26
  srs: srsContract,
27
+ stats: statsContract,
26
28
  user: userContract,
27
29
  videoTranscript: videoTranscriptContract,
28
30
  }
@@ -38,8 +40,11 @@ export * from "./schemas/hosted-ai"
38
40
  export * from "./schemas/notebase"
39
41
  export * from "./schemas/notebase-column"
40
42
  export * from "./schemas/notebase-row"
43
+ export * from "./schemas/notebase-stats"
41
44
  export * from "./schemas/notebase-view"
42
45
  export * from "./schemas/srs"
46
+ export * from "./schemas/srs-stats"
47
+ export * from "./schemas/stats"
43
48
  export * from "./schemas/timezone"
44
49
  export * from "./schemas/user"
45
50
  export * from "./schemas/video-transcript"
@@ -1,5 +1,6 @@
1
1
  import type { ErrorMap } from "@orpc/contract"
2
2
  import { z } from "zod"
3
+ import { HostedAiCreditPolicyIdSchema, HostedAiModelTierSchema } from "./schemas/hosted-ai"
3
4
 
4
5
  interface PublicAppErrorDefinition {
5
6
  data?: z.ZodTypeAny
@@ -38,7 +39,32 @@ export const CardTemplateInvalidColumnsDataSchema = z.object({
38
39
  }).strict()
39
40
  export type CardTemplateInvalidColumnsData = z.infer<typeof CardTemplateInvalidColumnsDataSchema>
40
41
 
42
+ export const HostedAiTierRestrictedDataSchema = z.strictObject({
43
+ modelTier: HostedAiModelTierSchema,
44
+ })
45
+
46
+ export const HostedAiQuotaExhaustedDataSchema = z.strictObject({
47
+ modelTier: HostedAiModelTierSchema,
48
+ // Reuses the credit-status enum rather than restating it: these two always
49
+ // describe the same set of policy ids, and a second copy silently drifts —
50
+ // the error payload is `strictObject`, so a policy missing only here fails
51
+ // output validation at exactly the moment a user runs out of credit.
52
+ policyId: HostedAiCreditPolicyIdSchema,
53
+ usedPercent: z.number().min(0).max(100),
54
+ resetAt: z.iso.datetime().nullable(),
55
+ })
56
+
41
57
  export const PUBLIC_APP_ERROR_DEFS = {
58
+ HOSTED_AI_TIER_RESTRICTED: {
59
+ data: HostedAiTierRestrictedDataSchema,
60
+ message: "This Built-in AI tier is not available for this account",
61
+ status: 403,
62
+ },
63
+ HOSTED_AI_QUOTA_EXHAUSTED: {
64
+ data: HostedAiQuotaExhaustedDataSchema,
65
+ message: "Built-in AI credit exhausted",
66
+ status: 429,
67
+ },
42
68
  NOTEBASE_BETA_RESTRICTED: {
43
69
  message: "Notebase is currently in beta for selected accounts",
44
70
  status: 403,
@@ -1,8 +1,5 @@
1
- import type { RefinementCtx } from "zod"
2
1
  import { z } from "zod"
3
2
 
4
- const HOSTED_AI_SYSTEM_FALLBACK_REMOVAL_DATE = "2026-07-11"
5
-
6
3
  export const HostedAiOutputFieldTypeSchema = z.enum(["string", "number"])
7
4
 
8
5
  export const HostedAiOutputFieldSchema = z.strictObject({
@@ -12,42 +9,43 @@ export const HostedAiOutputFieldSchema = z.strictObject({
12
9
 
13
10
  const HostedAiInstructionsSchema = z.string().trim().min(1).max(16000)
14
11
 
15
- interface HostedAiInstructionsCompatibilityInput {
16
- instructions?: string
17
- system?: string
18
- }
19
-
20
- function validateHostedAiInstructionsCompatibility(
21
- input: HostedAiInstructionsCompatibilityInput,
22
- ctx: RefinementCtx<HostedAiInstructionsCompatibilityInput>,
23
- ): void {
24
- if (input.instructions !== undefined || input.system !== undefined) {
25
- return
26
- }
27
-
28
- ctx.addIssue({
29
- code: "custom",
30
- message: `instructions is required. The deprecated system field is accepted as a temporary fallback until ${HOSTED_AI_SYSTEM_FALLBACK_REMOVAL_DATE}.`,
31
- path: ["instructions"],
32
- })
12
+ export const HostedAiModelTierSchema = z.enum(["normal", "ultra"])
13
+ export const HostedAiFeatureSchema = z.enum([
14
+ "pageTranslation",
15
+ "customAction",
16
+ "noteSuggestion",
17
+ ])
18
+ export const HostedAiRequestIdSchema = z.uuid()
19
+
20
+ const HostedAiRequestMetadataSchema = {
21
+ /**
22
+ * Optional on the wire, always present after parsing: these endpoints shipped
23
+ * before tiers existed, so every installed extension below 1.55.0 sends no
24
+ * `modelTier` at all, and the input objects are strict. Defaulting here keeps
25
+ * those clients working while giving handlers a single source of truth
26
+ * instead of a `?? "normal"` at every call site.
27
+ *
28
+ * TODO(2026-11-01): Drop the default and make this required once extensions
29
+ * below 1.55.0 have drained from the store.
30
+ */
31
+ modelTier: HostedAiModelTierSchema.default("normal"),
32
+ requestId: HostedAiRequestIdSchema.optional(),
33
33
  }
34
34
 
35
35
  export const HostedAiStreamTextInputSchema = z.strictObject({
36
- instructions: HostedAiInstructionsSchema.optional(),
37
- system: HostedAiInstructionsSchema.optional(),
36
+ ...HostedAiRequestMetadataSchema,
37
+ instructions: HostedAiInstructionsSchema,
38
38
  prompt: z.string().trim().min(1).max(32000),
39
39
  temperature: z.number().min(0).max(2).optional(),
40
- }).superRefine(validateHostedAiInstructionsCompatibility)
40
+ })
41
41
 
42
42
  export const HostedAiStreamStructuredObjectInputSchema = z.strictObject({
43
- instructions: HostedAiInstructionsSchema.optional(),
44
- system: HostedAiInstructionsSchema.optional(),
43
+ ...HostedAiRequestMetadataSchema,
44
+ instructions: HostedAiInstructionsSchema,
45
45
  prompt: z.string().trim().min(1).max(32000),
46
46
  outputSchema: z.array(HostedAiOutputFieldSchema).min(1).max(32),
47
47
  temperature: z.number().min(0).max(2).optional(),
48
48
  }).superRefine((input, ctx) => {
49
- validateHostedAiInstructionsCompatibility(input, ctx)
50
-
51
49
  const fieldNames = new Set<string>()
52
50
 
53
51
  input.outputSchema.forEach((field, index) => {
@@ -68,6 +66,7 @@ export const HOSTED_AI_NOTE_SUGGESTION_MAX_NOTES = 2
68
66
  export const HOSTED_AI_NOTE_SUGGESTION_MAX_NOTE_FIELDS = 16
69
67
 
70
68
  export const HostedAiNoteSuggestionStreamInputSchema = z.strictObject({
69
+ ...HostedAiRequestMetadataSchema,
71
70
  instructions: HostedAiInstructionsSchema,
72
71
  prompt: z.string().trim().min(1).max(32000),
73
72
  temperature: z.number().min(0).max(2).optional(),
@@ -109,10 +108,126 @@ export const HostedAiStreamPartSchema = z.looseObject({
109
108
  type: z.string().trim().min(1),
110
109
  })
111
110
 
111
+ /**
112
+ * The only stream part types this API emits. Every other part the AI SDK
113
+ * produces (`start-step`, `finish-step`, `text-start`, ...) carries upstream
114
+ * metadata — provider model id, response headers, request body — and is dropped
115
+ * server-side so it never reaches a client.
116
+ *
117
+ * The consumer switch lives in the extension's background stream reader. Adding
118
+ * a type here without teaching that switch about it is a no-op; removing one
119
+ * breaks rendering. `error` is absent on purpose: it is converted into a thrown
120
+ * error before it can be yielded.
121
+ */
122
+ export const HOSTED_AI_CLIENT_STREAM_PART_TYPES = [
123
+ "text-delta",
124
+ "reasoning-start",
125
+ "reasoning-delta",
126
+ "reasoning-end",
127
+ "reasoning-file",
128
+ "finish",
129
+ ] as const
130
+
131
+ /**
132
+ * Status is per-identity, not per-feature: one call reports every feature and
133
+ * every pool, so a client caches a single response instead of one per feature.
134
+ * Non-strict so a client still sending the retired `feature` argument gets a
135
+ * useful response rather than a validation error.
136
+ */
137
+ export const HostedAiStatusInputSchema = z.object({})
138
+
139
+ export const HostedAiUnavailableReasonSchema = z.enum([
140
+ "authentication_required",
141
+ "ultra_required",
142
+ "quota_exhausted",
143
+ "service_unavailable",
144
+ ])
145
+
146
+ export const HostedAiCreditPolicyIdSchema = z.enum([
147
+ "custom-action-guest-daily-v1",
148
+ "custom-action-user-daily-v1",
149
+ "custom-action-pro-daily-v1",
150
+ "ultra-weekly-v1",
151
+ ])
152
+
153
+ export const HostedAiCreditPeriodKindSchema = z.enum(["daily", "weekly"])
154
+
155
+ export const HostedAiQuotaScopeSchema = z.enum(["guest", "user"])
156
+
157
+ /**
158
+ * Data payload of the TOO_MANY_REQUESTS error every hosted AI guard emits.
159
+ * The code itself is ORPC's own rather than a PUBLIC_APP_ERROR_DEFS entry, so
160
+ * this schema is the only contract tying the server's construction to the
161
+ * extension's parsing of it.
162
+ */
163
+ export const HostedAiRateLimitErrorDataSchema = z.strictObject({
164
+ retryAfterMs: z.number().int().min(0),
165
+ quotaScope: HostedAiQuotaScopeSchema,
166
+ })
167
+
168
+ /**
169
+ * Deliberately free of dollar amounts and of the versioned policy id. Clients
170
+ * render a consumption bar and a reset time, so a percentage and a period kind
171
+ * are all they need, and neither leaks what a call costs us. `usedPercent`
172
+ * counts settled spend only: including in-flight reservations would make the
173
+ * bar jump forward and then back as each stream settles.
174
+ */
175
+ export const HostedAiCreditStatusSchema = z.strictObject({
176
+ periodKind: HostedAiCreditPeriodKindSchema,
177
+ usedPercent: z.number().min(0).max(100),
178
+ resetAt: z.iso.datetime().nullable(),
179
+ /**
180
+ * Every feature drawing on this pool. Ultra shares one weekly pool across all
181
+ * three, so the grouping lives here rather than inlined per feature — that way
182
+ * a client can tell one shared pool from several separate ones.
183
+ */
184
+ features: z.array(HostedAiFeatureSchema).min(1),
185
+ })
186
+
187
+ export const HostedAiTierStatusSchema = z.strictObject({
188
+ accessAllowed: z.boolean(),
189
+ available: z.boolean(),
190
+ unavailableReason: HostedAiUnavailableReasonSchema.nullable(),
191
+ /**
192
+ * Viewer-independent product fact: no non-Ultra account can ever use this
193
+ * feature/tier, so signing in alone will not unlock it. Unlike the fields
194
+ * above, this does not vary with the caller's identity or plan — clients use
195
+ * it to label Ultra-only options rather than to gate the current user.
196
+ */
197
+ requiresUltra: z.boolean(),
198
+ modelRevision: z.string().trim().min(1),
199
+ })
200
+
201
+ export const HostedAiFeatureStatusSchema = z.strictObject({
202
+ normal: HostedAiTierStatusSchema,
203
+ ultra: HostedAiTierStatusSchema,
204
+ })
205
+
206
+ export const HostedAiStatusOutputSchema = z.strictObject({
207
+ credits: z.array(HostedAiCreditStatusSchema),
208
+ features: z.strictObject({
209
+ pageTranslation: HostedAiFeatureStatusSchema,
210
+ customAction: HostedAiFeatureStatusSchema,
211
+ noteSuggestion: HostedAiFeatureStatusSchema,
212
+ }),
213
+ })
214
+
112
215
  export type HostedAiOutputFieldType = z.infer<typeof HostedAiOutputFieldTypeSchema>
113
216
  export type HostedAiOutputField = z.infer<typeof HostedAiOutputFieldSchema>
217
+ export type HostedAiModelTier = z.infer<typeof HostedAiModelTierSchema>
218
+ export type HostedAiFeature = z.infer<typeof HostedAiFeatureSchema>
114
219
  export type HostedAiStreamTextInput = z.infer<typeof HostedAiStreamTextInputSchema>
115
220
  export type HostedAiStreamStructuredObjectInput = z.infer<typeof HostedAiStreamStructuredObjectInputSchema>
116
221
  export type HostedAiNoteSuggestionStreamInput = z.infer<typeof HostedAiNoteSuggestionStreamInputSchema>
117
222
  export type HostedAiNoteSuggestionObject = z.infer<typeof HostedAiNoteSuggestionObjectSchema>
118
223
  export type HostedAiStreamPart = z.infer<typeof HostedAiStreamPartSchema>
224
+ export type HostedAiStatusInput = z.infer<typeof HostedAiStatusInputSchema>
225
+ export type HostedAiUnavailableReason = z.infer<typeof HostedAiUnavailableReasonSchema>
226
+ export type HostedAiCreditPolicyId = z.infer<typeof HostedAiCreditPolicyIdSchema>
227
+ export type HostedAiCreditPeriodKind = z.infer<typeof HostedAiCreditPeriodKindSchema>
228
+ export type HostedAiQuotaScope = z.infer<typeof HostedAiQuotaScopeSchema>
229
+ export type HostedAiRateLimitErrorData = z.infer<typeof HostedAiRateLimitErrorDataSchema>
230
+ export type HostedAiCreditStatus = z.infer<typeof HostedAiCreditStatusSchema>
231
+ export type HostedAiTierStatus = z.infer<typeof HostedAiTierStatusSchema>
232
+ export type HostedAiFeatureStatus = z.infer<typeof HostedAiFeatureStatusSchema>
233
+ export type HostedAiStatusOutput = z.infer<typeof HostedAiStatusOutputSchema>
@@ -11,6 +11,7 @@ export const notebaseColumnSchema = z.object({
11
11
  position: z.number(),
12
12
  isPrimary: z.boolean(),
13
13
  width: z.number().nullable(),
14
+ wrap: z.boolean(),
14
15
  createdAt: z.coerce.date(),
15
16
  updatedAt: z.coerce.date(),
16
17
  })
@@ -27,6 +28,7 @@ export const notebaseColumnUpdateDataSchema = z.object({
27
28
  name: z.string().min(1).optional(),
28
29
  config: notebaseColumnConfigSchema.optional(),
29
30
  width: notebaseColumnWidthSchema.nullable().optional(),
31
+ wrap: z.boolean().optional(),
30
32
  }).strict()
31
33
  export type NotebaseColumnUpdateData = z.infer<typeof notebaseColumnUpdateDataSchema>
32
34
 
@@ -0,0 +1,24 @@
1
+ import { z } from "zod"
2
+
3
+ export const notebaseActivityStatsItemSchema = z.object({
4
+ date: z.iso.date(),
5
+ notes: z.number().int().nonnegative(),
6
+ }).strict()
7
+ export type NotebaseActivityStatsItem = z.infer<typeof notebaseActivityStatsItemSchema>
8
+
9
+ export const notebaseActivityStatsOutputSchema = z.object({
10
+ daily: z.array(notebaseActivityStatsItemSchema),
11
+ }).strict()
12
+ export type NotebaseActivityStatsOutput = z.infer<typeof notebaseActivityStatsOutputSchema>
13
+
14
+ export const notebaseRowGrowthStatsItemSchema = z.object({
15
+ date: z.iso.date(),
16
+ delta: z.number().int(),
17
+ }).strict()
18
+ export type NotebaseRowGrowthStatsItem = z.infer<typeof notebaseRowGrowthStatsItemSchema>
19
+
20
+ export const notebaseRowGrowthStatsOutputSchema = z.object({
21
+ baseTotal: z.number().int().nonnegative(),
22
+ daily: z.array(notebaseRowGrowthStatsItemSchema),
23
+ }).strict()
24
+ export type NotebaseRowGrowthStatsOutput = z.infer<typeof notebaseRowGrowthStatsOutputSchema>
@@ -0,0 +1,26 @@
1
+ import { z } from "zod"
2
+
3
+ export const srsActivityStatsItemSchema = z.object({
4
+ date: z.iso.date(),
5
+ answers: z.number().int().nonnegative(),
6
+ durationMs: z.number().int().nonnegative(),
7
+ }).strict()
8
+ export type SrsActivityStatsItem = z.infer<typeof srsActivityStatsItemSchema>
9
+
10
+ export const srsActivityStatsOutputSchema = z.object({
11
+ learnedCards: z.number().int().nonnegative(),
12
+ daily: z.array(srsActivityStatsItemSchema),
13
+ }).strict()
14
+ export type SrsActivityStatsOutput = z.infer<typeof srsActivityStatsOutputSchema>
15
+
16
+ export const srsMemoryGrowthStatsItemSchema = z.object({
17
+ date: z.iso.date(),
18
+ delta: z.number().int(),
19
+ }).strict()
20
+ export type SrsMemoryGrowthStatsItem = z.infer<typeof srsMemoryGrowthStatsItemSchema>
21
+
22
+ export const srsMemoryGrowthStatsOutputSchema = z.object({
23
+ baseTotal: z.number().int().nonnegative(),
24
+ daily: z.array(srsMemoryGrowthStatsItemSchema),
25
+ }).strict()
26
+ export type SrsMemoryGrowthStatsOutput = z.infer<typeof srsMemoryGrowthStatsOutputSchema>
@@ -90,7 +90,9 @@ export const srsRevlogSchema = z.object({
90
90
  cardId: z.uuid(),
91
91
  rating: reviewRatingSchema,
92
92
  state: cardStateSchema,
93
+ stability: z.number(),
93
94
  afterScheduleStatus: scheduleStatusSchema,
95
+ afterStability: z.number(),
94
96
  reviewedAt: z.coerce.date(),
95
97
  durationMs: z.number().int(),
96
98
  fsrsReviewLogSnapshot: fsrsReviewLogSnapshotSchema,
@@ -103,6 +105,16 @@ export const srsRevlogWithoutSnapshotSchema = srsRevlogSchema.omit({
103
105
  })
104
106
  export type SrsRevlogWithoutSnapshot = z.infer<typeof srsRevlogWithoutSnapshotSchema>
105
107
 
108
+ export const srsListCardRevlogsInputSchema = z.object({
109
+ cardId: z.uuid(),
110
+ }).strict()
111
+ export type SrsListCardRevlogsInput = z.infer<typeof srsListCardRevlogsInputSchema>
112
+
113
+ export const srsListCardRevlogsOutputSchema = z.object({
114
+ items: z.array(srsRevlogWithoutSnapshotSchema),
115
+ }).strict()
116
+ export type SrsListCardRevlogsOutput = z.infer<typeof srsListCardRevlogsOutputSchema>
117
+
106
118
  export const srsReviewOutputSchema = z.object({
107
119
  card: cardSchema,
108
120
  revlog: srsRevlogSchema,
@@ -0,0 +1,25 @@
1
+ import { z } from "zod"
2
+ import { notebaseActivityStatsOutputSchema } from "./notebase-stats"
3
+ import { srsActivityStatsOutputSchema } from "./srs-stats"
4
+ import { timezoneSchema } from "./timezone"
5
+
6
+ const statsNotebaseIdsSchema = z.array(z.uuid())
7
+ .min(1)
8
+ .transform(ids => Array.from(new Set(ids)))
9
+
10
+ export const statsInputSchema = z.object({
11
+ notebaseIds: statsNotebaseIdsSchema.optional(),
12
+ from: z.iso.date().optional(),
13
+ to: z.iso.date(),
14
+ timezone: timezoneSchema,
15
+ }).strict().refine(input => !input.from || input.from <= input.to, {
16
+ message: "from must be before or equal to to",
17
+ path: ["from"],
18
+ })
19
+ export type StatsInput = z.infer<typeof statsInputSchema>
20
+
21
+ export const activityStatsOutputSchema = z.object({
22
+ srs: srsActivityStatsOutputSchema,
23
+ notebase: notebaseActivityStatsOutputSchema,
24
+ }).strict()
25
+ export type ActivityStatsOutput = z.infer<typeof activityStatsOutputSchema>