@read-frog/api-contract 0.18.1 → 0.20.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@read-frog/api-contract",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@orpc/contract": "^1.15.2",
28
- "@read-frog/definitions": "0.7.0",
28
+ "@read-frog/definitions": "0.9.0",
29
29
  "zod": "^4.6.5"
30
30
  },
31
31
  "devDependencies": {
@@ -42,10 +42,16 @@ export const cardTemplateContract = {
42
42
  path: "/card-templates",
43
43
  summary: "Create card template",
44
44
  description:
45
- "Create a template in an owned notebase using existing field IDs in its patterns. This generates cards for existing notes.",
45
+ 'Create a template in an owned notebase; it generates a card for every existing note. Each face (front, back) is either { columns: [column IDs], style }, those columns in a built-in style that stays editable in the website\'s simple mode, or { layout: "<HTML + Liquid>" } for a custom design that references a column as {{ ["<column id>"] }}. Prefer columns faces. Get column IDs from the notebase schema.',
46
46
  tags: ["Card Templates"],
47
47
  })
48
- .errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND", "CARD_TEMPLATE_INVALID_NOTEBASE_COLUMNS"))
48
+ .errors(
49
+ pickPublicErrorMap(
50
+ "NOTEBASE_NOT_FOUND",
51
+ "CARD_TEMPLATE_INVALID_NOTEBASE_COLUMNS",
52
+ "CARD_TEMPLATE_INVALID_LAYOUT",
53
+ ),
54
+ )
49
55
  .errors(pickPublicErrorMap("RESOURCE_ALREADY_EXISTS"))
50
56
  .input(CardTemplateCreateInputSchema)
51
57
  .output(CardTemplateCreateOutputSchema),
@@ -55,10 +61,16 @@ export const cardTemplateContract = {
55
61
  path: "/card-templates/{id}",
56
62
  summary: "Update card template",
57
63
  description:
58
- "Change an owned template name or definition. Rendered cards reflect the saved template. expectedUpdatedAt guards against editing stale data.",
64
+ "Change an owned template name or faces (see creating a template for the face shapes). Rendered cards reflect the saved template. expectedUpdatedAt guards against editing stale data.",
59
65
  tags: ["Card Templates"],
60
66
  })
61
- .errors(pickPublicErrorMap("CARD_TEMPLATE_NOT_FOUND", "CARD_TEMPLATE_INVALID_NOTEBASE_COLUMNS"))
67
+ .errors(
68
+ pickPublicErrorMap(
69
+ "CARD_TEMPLATE_NOT_FOUND",
70
+ "CARD_TEMPLATE_INVALID_NOTEBASE_COLUMNS",
71
+ "CARD_TEMPLATE_INVALID_LAYOUT",
72
+ ),
73
+ )
62
74
  .errors(pickPublicErrorMap("RESOURCE_UPDATE_CONFLICT"))
63
75
  .input(CardTemplateUpdateInputSchema)
64
76
  .output(CardTemplateUpdateOutputSchema),
@@ -0,0 +1,22 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { masteryGoalOutputSchema, masteryGoalUpdateInputSchema } from "#/schemas/mastery-goal";
3
+
4
+ export const masteryGoalContract = {
5
+ get: oc
6
+ .route({
7
+ method: "GET",
8
+ path: "/mastery-goal",
9
+ summary: "Get the authenticated user's mastered-cards goal",
10
+ tags: ["Goals"],
11
+ })
12
+ .output(masteryGoalOutputSchema),
13
+ update: oc
14
+ .route({
15
+ method: "PUT",
16
+ path: "/mastery-goal",
17
+ summary: "Set or clear the authenticated user's mastered-cards goal",
18
+ tags: ["Goals"],
19
+ })
20
+ .input(masteryGoalUpdateInputSchema)
21
+ .output(masteryGoalOutputSchema),
22
+ };
@@ -8,6 +8,8 @@ import {
8
8
  srsListCardRevlogsOutputSchema,
9
9
  srsListUnreadOptimizerResultsInputSchema,
10
10
  srsListUnreadOptimizerResultsOutputSchema,
11
+ srsRecentNotebaseScheduleStatusStatsInputSchema,
12
+ srsRecentNotebaseScheduleStatusStatsOutputSchema,
11
13
  srsReviewInputSchema,
12
14
  srsReviewOutputSchema,
13
15
  srsRollbackReviewInputSchema,
@@ -32,6 +34,17 @@ export const srsContract = {
32
34
  .errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND"))
33
35
  .input(srsScheduleStatusStatsInputSchema)
34
36
  .output(srsScheduleStatusStatsOutputSchema),
37
+ recentNotebaseScheduleStatusStats: oc
38
+ .route({
39
+ method: "GET",
40
+ path: "/srs/schedule-status-stats/recent",
41
+ summary: "Get study queue counts of the most recently used notebase",
42
+ description:
43
+ "Picks the owned notebase with the latest review or content edit (notes, fields or templates) and returns its study queue counts in the supplied timezone.",
44
+ tags: ["SRS"],
45
+ })
46
+ .input(srsRecentNotebaseScheduleStatusStatsInputSchema)
47
+ .output(srsRecentNotebaseScheduleStatusStatsOutputSchema),
35
48
  listUnreadOptimizerResults: oc
36
49
  .route({
37
50
  method: "GET",
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { cardContract } from "./contracts/card";
5
5
  import { cardTemplateContract } from "./contracts/card-template";
6
6
  import { ebookContract } from "./contracts/ebook";
7
7
  import { hostedAiContract } from "./contracts/hosted-ai";
8
+ import { masteryGoalContract } from "./contracts/mastery-goal";
8
9
  import { mcpConnectionContract } from "./contracts/mcp-connection";
9
10
  import { notebaseContract } from "./contracts/notebase";
10
11
  import { notebaseColumnContract } from "./contracts/notebase-column";
@@ -21,6 +22,7 @@ export const contract = {
21
22
  cardTemplate: cardTemplateContract,
22
23
  ebook: ebookContract,
23
24
  hostedAi: hostedAiContract,
25
+ masteryGoal: masteryGoalContract,
24
26
  mcpConnection: mcpConnectionContract,
25
27
  notebase: notebaseContract,
26
28
  notebaseColumn: notebaseColumnContract,
@@ -40,6 +42,7 @@ export * from "./schemas/billing";
40
42
  export * from "./schemas/card";
41
43
  export * from "./schemas/ebook";
42
44
  export * from "./schemas/hosted-ai";
45
+ export * from "./schemas/mastery-goal";
43
46
  export * from "./schemas/mcp-connection";
44
47
  export * from "./schemas/notebase";
45
48
  export * from "./schemas/notebase-column";
@@ -45,6 +45,33 @@ export const CardTemplateInvalidColumnsDataSchema = z
45
45
  .strict();
46
46
  export type CardTemplateInvalidColumnsData = z.infer<typeof CardTemplateInvalidColumnsDataSchema>;
47
47
 
48
+ export const CardTemplateInvalidLayoutDataSchema = z
49
+ .object({
50
+ errors: z.array(
51
+ z
52
+ .object({
53
+ side: z.enum(["front", "back"]),
54
+ message: z.string(),
55
+ // UTF-16 offsets into that face's layout, when the parser knows them.
56
+ from: z.number().int().min(0).optional(),
57
+ to: z.number().int().min(0).optional(),
58
+ })
59
+ .strict(),
60
+ ),
61
+ })
62
+ .strict();
63
+ export type CardTemplateInvalidLayoutData = z.infer<typeof CardTemplateInvalidLayoutDataSchema>;
64
+
65
+ // The templates that use the column, which keep it from being deleted.
66
+ export const CardTemplateNotebaseColumnInUseDataSchema = z
67
+ .object({
68
+ templates: z.array(z.object({ id: z.uuid(), name: z.string() }).strict()),
69
+ })
70
+ .strict();
71
+ export type CardTemplateNotebaseColumnInUseData = z.infer<
72
+ typeof CardTemplateNotebaseColumnInUseDataSchema
73
+ >;
74
+
48
75
  export const HostedAiTierRestrictedDataSchema = z.strictObject({
49
76
  modelTier: HostedAiModelTierSchema,
50
77
  });
@@ -148,7 +175,13 @@ export const PUBLIC_APP_ERROR_DEFS = {
148
175
  message: "Card template references unknown notebase columns",
149
176
  status: 422,
150
177
  },
178
+ CARD_TEMPLATE_INVALID_LAYOUT: {
179
+ data: CardTemplateInvalidLayoutDataSchema,
180
+ message: "Card template layout does not parse",
181
+ status: 422,
182
+ },
151
183
  CARD_TEMPLATE_NOTEBASE_COLUMN_IN_USE: {
184
+ data: CardTemplateNotebaseColumnInUseDataSchema,
152
185
  message: "Notebase column is used by a card template",
153
186
  status: 409,
154
187
  },
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+
3
+ export const MASTERY_GOAL_TARGET_MAX = 1_000_000;
4
+
5
+ /** How many mastered cards to aim for: the cards `stats.srsMemoryGrowth` counts. */
6
+ export const masteryGoalTargetSchema = z.number().int().min(1).max(MASTERY_GOAL_TARGET_MAX);
7
+ export type MasteryGoalTarget = z.infer<typeof masteryGoalTargetSchema>;
8
+
9
+ /** `null` clears the goal. */
10
+ export const masteryGoalUpdateInputSchema = z
11
+ .object({
12
+ target: masteryGoalTargetSchema.nullable(),
13
+ })
14
+ .strict();
15
+ export type MasteryGoalUpdateInput = z.infer<typeof masteryGoalUpdateInputSchema>;
16
+
17
+ /** The goal's target, or `null` while none is set. */
18
+ export const masteryGoalOutputSchema = z
19
+ .object({
20
+ target: masteryGoalTargetSchema.nullable(),
21
+ })
22
+ .strict();
23
+ export type MasteryGoalOutput = z.infer<typeof masteryGoalOutputSchema>;
@@ -2,6 +2,7 @@ import { deletionResultSchema } from "./resource-mutation";
2
2
  import {
3
3
  notebaseColumnConfigSchema,
4
4
  schedulingParamsShape,
5
+ srsModelVersionSchema,
5
6
  srsWeightsSchema,
6
7
  } from "@read-frog/definitions";
7
8
  import { z } from "zod";
@@ -38,6 +39,8 @@ export const notebaseSchema = z.object({
38
39
  srsLeechThreshold: schedulingParamsShape.leechThreshold,
39
40
  srsEnableFuzz: schedulingParamsShape.enableFuzz,
40
41
  srsWeights: srsWeightsSchema.nullable(),
42
+ // Selects the scheduler/curve; read-only, changed only by the FSRS-7 migration.
43
+ srsModelVersion: srsModelVersionSchema,
41
44
  createdAt: z.date(),
42
45
  updatedAt: z.date(),
43
46
  });
@@ -3,6 +3,7 @@ import {
3
3
  activeScheduleStatusSchema,
4
4
  cardStateSchema,
5
5
  fsrsReviewLogSnapshotSchema,
6
+ fsrsMemoryStateSchema,
6
7
  reviewRatingSchema,
7
8
  scheduleStatusSchema,
8
9
  schedulingParamsSchema,
@@ -11,6 +12,7 @@ import {
11
12
  } from "@read-frog/definitions";
12
13
  import { z } from "zod";
13
14
  import { cardSchema } from "./card";
15
+ import { notebaseSummarySchema } from "./notebase";
14
16
  import { timezoneSchema } from "./timezone";
15
17
 
16
18
  export { reviewRatingSchema, schedulingParamsSchema, SRS_REVIEW_DURATION_MS_MAX };
@@ -73,6 +75,26 @@ export type SrsScheduleStatusStatsInput = z.infer<typeof srsScheduleStatusStatsI
73
75
  export const srsScheduleStatusStatsOutputSchema = z.record(z.uuid(), srsScheduleStatusStatsSchema);
74
76
  export type SrsScheduleStatusStatsOutput = z.infer<typeof srsScheduleStatusStatsOutputSchema>;
75
77
 
78
+ export const srsRecentNotebaseScheduleStatusStatsInputSchema = z
79
+ .object({
80
+ timezone: timezoneSchema,
81
+ })
82
+ .strict();
83
+ export type SrsRecentNotebaseScheduleStatusStatsInput = z.infer<
84
+ typeof srsRecentNotebaseScheduleStatusStatsInputSchema
85
+ >;
86
+
87
+ /** Null when the user has no notebase yet. */
88
+ export const srsRecentNotebaseScheduleStatusStatsOutputSchema = z
89
+ .object({
90
+ notebase: notebaseSummarySchema,
91
+ stats: srsScheduleStatusStatsSchema,
92
+ })
93
+ .nullable();
94
+ export type SrsRecentNotebaseScheduleStatusStatsOutput = z.infer<
95
+ typeof srsRecentNotebaseScheduleStatusStatsOutputSchema
96
+ >;
97
+
76
98
  export const srsManualCardStatusInputSchema = z
77
99
  .object({
78
100
  cardId: z.uuid(),
@@ -95,7 +117,9 @@ export const srsRevlogSchema = z.object({
95
117
  state: cardStateSchema,
96
118
  stability: z.number(),
97
119
  afterScheduleStatus: scheduleStatusSchema,
98
- afterStability: z.number(),
120
+ afterMemoryState: fsrsMemoryStateSchema,
121
+ retentionDays90: z.number().nonnegative(),
122
+ afterRetentionDays90: z.number().nonnegative(),
99
123
  reviewedAt: z.coerce.date(),
100
124
  durationMs: z.number().int(),
101
125
  fsrsReviewLogSnapshot: fsrsReviewLogSnapshotSchema,