@studious-lms/server 1.15.0 → 1.16.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.
Files changed (113) hide show
  1. package/.github/workflows/deploy.yml +1 -1
  2. package/dist/instrument.js +3 -3
  3. package/dist/instrument.js.map +1 -1
  4. package/dist/models/announcement.d.ts +0 -2
  5. package/dist/models/announcement.d.ts.map +1 -1
  6. package/dist/models/auth.d.ts +0 -8
  7. package/dist/models/auth.d.ts.map +1 -1
  8. package/dist/models/auth.js +2 -4
  9. package/dist/models/auth.js.map +1 -1
  10. package/dist/models/event.d.ts +0 -2
  11. package/dist/models/event.d.ts.map +1 -1
  12. package/dist/models/file.d.ts +0 -24
  13. package/dist/models/file.d.ts.map +1 -1
  14. package/dist/models/marketing.d.ts +15 -2
  15. package/dist/models/marketing.d.ts.map +1 -1
  16. package/dist/models/marketing.js +4 -12
  17. package/dist/models/marketing.js.map +1 -1
  18. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  19. package/dist/pipelines/aiLabChat.js +26 -34
  20. package/dist/pipelines/aiLabChat.js.map +1 -1
  21. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  22. package/dist/pipelines/gradeWorksheet.js +75 -36
  23. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  24. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  25. package/dist/pipelines/labChatPrompt.js +3 -6
  26. package/dist/pipelines/labChatPrompt.js.map +1 -1
  27. package/dist/routers/_app.d.ts +26 -222
  28. package/dist/routers/_app.d.ts.map +1 -1
  29. package/dist/routers/assignment.d.ts +0 -10
  30. package/dist/routers/assignment.d.ts.map +1 -1
  31. package/dist/routers/auth.d.ts +0 -2
  32. package/dist/routers/auth.d.ts.map +1 -1
  33. package/dist/routers/auth.js +2 -15
  34. package/dist/routers/auth.js.map +1 -1
  35. package/dist/routers/class.d.ts +0 -4
  36. package/dist/routers/class.d.ts.map +1 -1
  37. package/dist/routers/event.d.ts +0 -2
  38. package/dist/routers/event.d.ts.map +1 -1
  39. package/dist/routers/marketing.d.ts +13 -0
  40. package/dist/routers/marketing.d.ts.map +1 -1
  41. package/dist/routers/school.d.ts +0 -4
  42. package/dist/routers/school.d.ts.map +1 -1
  43. package/dist/routers/user.d.ts +0 -89
  44. package/dist/routers/user.d.ts.map +1 -1
  45. package/dist/routers/user.js +2 -19
  46. package/dist/routers/user.js.map +1 -1
  47. package/dist/seedDatabase.d.ts +0 -2
  48. package/dist/seedDatabase.d.ts.map +1 -1
  49. package/dist/services/aiChatStream.d.ts +0 -2
  50. package/dist/services/aiChatStream.d.ts.map +1 -1
  51. package/dist/services/aiChatStream.js +4 -31
  52. package/dist/services/aiChatStream.js.map +1 -1
  53. package/dist/services/aiProposalTools.d.ts.map +1 -1
  54. package/dist/services/aiProposalTools.js +7 -7
  55. package/dist/services/aiProposalTools.js.map +1 -1
  56. package/dist/services/aiTools.js +3 -3
  57. package/dist/services/aiTools.js.map +1 -1
  58. package/dist/services/assignment.d.ts +0 -10
  59. package/dist/services/assignment.d.ts.map +1 -1
  60. package/dist/services/auth.d.ts +0 -2
  61. package/dist/services/auth.d.ts.map +1 -1
  62. package/dist/services/auth.js +4 -13
  63. package/dist/services/auth.js.map +1 -1
  64. package/dist/services/class.d.ts +0 -4
  65. package/dist/services/class.d.ts.map +1 -1
  66. package/dist/services/event.d.ts +0 -2
  67. package/dist/services/event.d.ts.map +1 -1
  68. package/dist/services/marketing.d.ts +14 -1
  69. package/dist/services/marketing.d.ts.map +1 -1
  70. package/dist/services/marketing.js +5 -5
  71. package/dist/services/marketing.js.map +1 -1
  72. package/dist/services/openaiDocumentGen.d.ts +0 -12
  73. package/dist/services/openaiDocumentGen.d.ts.map +1 -1
  74. package/dist/services/openaiDocumentGen.js +63 -130
  75. package/dist/services/openaiDocumentGen.js.map +1 -1
  76. package/dist/services/school.d.ts +0 -4
  77. package/dist/services/school.d.ts.map +1 -1
  78. package/dist/utils/email.d.ts.map +1 -1
  79. package/dist/utils/email.js +3 -7
  80. package/dist/utils/email.js.map +1 -1
  81. package/package.json +1 -1
  82. package/prisma/schema.prisma +0 -3
  83. package/src/instrument.ts +1 -1
  84. package/src/models/auth.ts +0 -4
  85. package/src/models/marketing.ts +2 -10
  86. package/src/pipelines/aiLabChat.ts +28 -41
  87. package/src/pipelines/gradeWorksheet.ts +95 -38
  88. package/src/pipelines/labChatPrompt.ts +1 -4
  89. package/src/routers/auth.ts +0 -13
  90. package/src/routers/user.ts +0 -26
  91. package/src/services/aiChatStream.ts +3 -30
  92. package/src/services/aiProposalTools.ts +5 -6
  93. package/src/services/aiTools.ts +1 -1
  94. package/src/services/auth.ts +2 -14
  95. package/src/services/marketing.ts +3 -3
  96. package/src/services/openaiDocumentGen.ts +64 -146
  97. package/src/utils/email.ts +1 -5
  98. package/tests/integration/globalSetup.ts +18 -10
  99. package/tests/integration/routers/auth.test.ts +18 -13
  100. package/dist/lib/legal.d.ts +0 -3
  101. package/dist/lib/legal.d.ts.map +0 -1
  102. package/dist/lib/legal.js +0 -6
  103. package/dist/lib/legal.js.map +0 -1
  104. package/dist/services/userPrivacy.d.ts +0 -86
  105. package/dist/services/userPrivacy.d.ts.map +0 -1
  106. package/dist/services/userPrivacy.js +0 -184
  107. package/dist/services/userPrivacy.js.map +0 -1
  108. package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +0 -3
  109. package/src/lib/legal.ts +0 -2
  110. package/src/services/userPrivacy.ts +0 -196
  111. package/tests/helpers/registerInput.ts +0 -20
  112. package/tests/unit/services/aiChatStream.test.ts +0 -92
  113. package/tests/unit/services/openaiDocumentGen.test.ts +0 -65
@@ -20,6 +20,63 @@ const removeAllPreviousAIComments = async (worksheetQuestionProgressId: string)
20
20
  });
21
21
  };
22
22
 
23
+ type MarkSchemeItem = { id: string; points: number; description?: string; text?: string };
24
+
25
+ const gradingResponseSchema = z.object({
26
+ isCorrect: z.boolean(),
27
+ points: z.number(),
28
+ markschemeState: z.array(z.object({
29
+ id: z.string(),
30
+ correct: z.boolean(),
31
+ })),
32
+ comments: z.array(z.string()),
33
+ annotations: z.array(z.object({
34
+ quote: z.string().describe('An exact substring from the student response that is relevant to this criterion.'),
35
+ markSchemeId: z.string().describe('The id of the mark scheme criterion this quote relates to.'),
36
+ criterion: z.string().describe('Short label describing what this mark scheme criterion evaluates.'),
37
+ met: z.boolean().describe('Whether the quoted text satisfies this criterion.'),
38
+ })).describe('Highlight specific parts of the student response and map each to a mark scheme criterion.'),
39
+ });
40
+
41
+ function buildGradingPrompt(
42
+ question: { question: string; answer: string; type: WorksheetQuestionType; points: number; markScheme: unknown },
43
+ responseText: string,
44
+ ): string {
45
+ const markSchemeItems = Array.isArray(question.markScheme) ? question.markScheme as MarkSchemeItem[] : [];
46
+ const markSchemeBlock = markSchemeItems.length > 0
47
+ ? markSchemeItems.map((item, i) =>
48
+ ` ${i + 1}. [id="${item.id}", ${item.points} pts] ${item.description || item.text || `Criterion ${i + 1}`}`
49
+ ).join('\n')
50
+ : ' No mark scheme provided — grade based on correctness and completeness.';
51
+
52
+ const maxPoints = question.points || markSchemeItems.reduce((sum, item) => sum + (item.points || 0), 0);
53
+
54
+ return `Grade the following student response.
55
+
56
+ Question type: ${question.type}
57
+ Maximum points: ${maxPoints}
58
+
59
+ QUESTION:
60
+ ${question.question}
61
+
62
+ CORRECT ANSWER:
63
+ ${question.answer || 'No answer key provided.'}
64
+
65
+ MARK SCHEME:
66
+ ${markSchemeBlock}
67
+
68
+ STUDENT RESPONSE:
69
+ ${responseText}
70
+
71
+ INSTRUCTIONS:
72
+ - Evaluate the student response against each mark scheme criterion.
73
+ - Total points must not exceed ${maxPoints}.
74
+ - For each criterion, find the exact substring in the student's response that is relevant (the "quote" field). Copy the text verbatim — do not paraphrase.
75
+ - If no part of the response addresses a criterion, still include an annotation with an empty quote and met=false.
76
+ - Provide constructive feedback comments.
77
+ - Mark isCorrect=true only if the student earned full or near-full marks.`;
78
+ }
79
+
23
80
  const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQuestionProgressId: string) => {
24
81
 
25
82
  await removeAllPreviousAIComments(worksheetQuestionProgressId);
@@ -83,38 +140,12 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
83
140
  return updatedProgress;
84
141
  }
85
142
 
86
- const comments = studentQuestionProgress.comments;
87
-
88
143
  try {
144
+ const prompt = buildGradingPrompt(question, responseText);
145
+
89
146
  const apiResponse = await inference(
90
- `Grade the following worksheet response:
91
-
92
- Question: ${question.question}
93
- Response: ${responseText}
94
-
95
- Comments: ${comments.map((comment) => comment.content).join('\n')}
96
- Mark Scheme: ${JSON.stringify(question.markScheme)}
97
-
98
- Justify your reasoning by including comment(s) and mark the question please.
99
- Return ONLY JSON in the following format (fill in the values as per the question):
100
- {
101
- "isCorrect": <boolean>,
102
- "points": <number>,
103
- "markschemeState": [
104
- { "id": <string>, "correct": <boolean> }
105
- ],
106
- "comments": [<string>, ...]
107
- }
108
- `,
109
- z.object({
110
- isCorrect: z.boolean(),
111
- points: z.number(),
112
- markschemeState: z.array(z.object({
113
- id: z.string(),
114
- correct: z.boolean(),
115
- })), // @note: this has to be converted to [id: string]: correct boolean
116
- comments: z.array(z.string()),
117
- }),
147
+ prompt,
148
+ gradingResponseSchema,
118
149
  {
119
150
  reason: 'worksheet_autograde',
120
151
  metadata: {
@@ -128,6 +159,27 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
128
159
  throw error;
129
160
  });
130
161
 
162
+ const maxPoints = question.points || 0;
163
+ const clampedPoints = Math.max(0, Math.min(Math.round((apiResponse as { points: number }).points), maxPoints));
164
+
165
+ const validMarkSchemeIds = new Set(
166
+ (Array.isArray(question.markScheme) ? question.markScheme as MarkSchemeItem[] : []).map(item => item.id),
167
+ );
168
+
169
+ const validatedMarkschemeState = (apiResponse as {
170
+ markschemeState: { id: string; correct: boolean }[];
171
+ }).markschemeState
172
+ .filter(item => validMarkSchemeIds.has(item.id))
173
+ .reduce((acc, curr) => {
174
+ acc["item-" + curr.id] = curr.correct;
175
+ return acc;
176
+ }, {} as Record<string, boolean>);
177
+
178
+ const annotations = (apiResponse as {
179
+ annotations: { quote: string; markSchemeId: string; criterion: string; met: boolean }[];
180
+ }).annotations
181
+ .filter(a => a.quote.length === 0 || responseText.includes(a.quote));
182
+
131
183
  const updatedStudentQuestionProgress = await prisma.studentQuestionProgress.update({
132
184
  where: { id: studentQuestionProgress.id, status: {
133
185
  not: {
@@ -137,13 +189,11 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
137
189
  data: {
138
190
  status: JobStatus.COMPLETED,
139
191
  isCorrect: (apiResponse as { isCorrect: boolean }).isCorrect,
140
- points: (apiResponse as { points: number }).points,
141
- markschemeState: (apiResponse as {
142
- markschemeState: { id: string; correct: boolean }[];
143
- }).markschemeState.reduce((acc, curr) => {
144
- acc["item-" + curr.id] = curr.correct;
145
- return acc;
146
- }, {} as Record<string, boolean>),
192
+ points: clampedPoints,
193
+ markschemeState: {
194
+ ...validatedMarkschemeState,
195
+ _annotations: annotations,
196
+ },
147
197
  comments: {
148
198
  create: (apiResponse as {
149
199
  comments: string[];
@@ -279,7 +329,14 @@ export const gradeWorksheetPipeline = async (worksheetResponseId: string) => {
279
329
  continue;
280
330
  }
281
331
 
282
- await gradeWorksheetQuestion(worksheetResponseId, response.id);
332
+ try {
333
+ await gradeWorksheetQuestion(worksheetResponseId, response.id);
334
+ } catch (questionError) {
335
+ logger.error('Failed to grade individual question, continuing with remaining questions', {
336
+ questionId: response.questionId,
337
+ error: questionError,
338
+ });
339
+ }
283
340
 
284
341
  };
285
342
  };
@@ -47,7 +47,6 @@ const LAB_CHAT_REFERENCE_RULES = [
47
47
 
48
48
  const LAB_CHAT_DOC_RULES = [
49
49
  "WHEN CREATING FILES (generate_document tool):",
50
- "- Call generate_document with title, content (markdown/plain), and format.",
51
50
  "- You can generate MANY file types — not just PDF. Always pass format explicitly.",
52
51
  "- Match format to what the teacher asked for:",
53
52
  " * slides / presentation / deck → pptx",
@@ -56,10 +55,8 @@ const LAB_CHAT_DOC_RULES = [
56
55
  " * printable worksheet / reading → pdf",
57
56
  " * diagram / chart image → png",
58
57
  " * notes / readme → md or txt",
59
- "- Supported: pdf, docx, xlsx, pptx, csv, md, html, txt, json, xml, tex, png, jpeg, gif, zip, py, js, ts, css, sh.",
60
- "- Use markdown structure in content: # headings, paragraphs, - bullets, 1. numbered lists.",
58
+ "- Supported: pdf, docx, xlsx, pptx, csv, md, html, txt, json, xml, png, jpeg, gif, zip, py, js, ts, …",
61
59
  "- Write complete classroom-ready content — no placeholders.",
62
- "- One file per tool call; split long packs into logical separate documents if needed.",
63
60
  "- In your spoken reply, name the actual format (e.g. \"PowerPoint\", \"spreadsheet\") — never say you only make PDFs.",
64
61
  ];
65
62
 
@@ -5,7 +5,6 @@ import {
5
5
  protectedProcedure,
6
6
  publicProcedure,
7
7
  } from "../trpc.js";
8
- import { CURRENT_TERMS_VERSION } from "../lib/legal.js";
9
8
  import {
10
9
  register,
11
10
  login,
@@ -28,16 +27,6 @@ const registerSchema = z
28
27
  email: z.string().email("Invalid email address"),
29
28
  password: z.string().min(6, "Password must be at least 6 characters"),
30
29
  confirmPassword: z.string(),
31
- agreeToTerms: z.literal(true, {
32
- errorMap: () => ({
33
- message: "You must accept the Terms of Service and Privacy Policy",
34
- }),
35
- }),
36
- termsVersion: z.literal(CURRENT_TERMS_VERSION, {
37
- errorMap: () => ({
38
- message: "Please accept the current Terms of Service and Privacy Policy",
39
- }),
40
- }),
41
30
  })
42
31
  .refine((data) => data.password === data.confirmPassword, {
43
32
  message: "Passwords don't match",
@@ -52,8 +41,6 @@ export const authRouter = createTRPCRouter({
52
41
  username: input.username,
53
42
  email: input.email,
54
43
  password: input.password,
55
- agreeToTerms: input.agreeToTerms,
56
- termsVersion: input.termsVersion,
57
44
  })
58
45
  ),
59
46
 
@@ -1,4 +1,3 @@
1
- import { TRPCError } from "@trpc/server";
2
1
  import { z } from "zod";
3
2
  import { createTRPCRouter, protectedProcedure } from "../trpc.js";
4
3
  import {
@@ -6,10 +5,6 @@ import {
6
5
  updateProfile,
7
6
  getUploadUrl,
8
7
  } from "../services/user.js";
9
- import {
10
- deleteUserAccount,
11
- exportUserData,
12
- } from "../services/userPrivacy.js";
13
8
 
14
9
  const fileUploadSchema = z.object({
15
10
  filePath: z.string().min(1, "File path is required"),
@@ -84,25 +79,4 @@ export const userRouter = createTRPCRouter({
84
79
  .mutation(({ ctx, input }) =>
85
80
  getUploadUrl(ctx.user!.id, input.fileName, input.fileType)
86
81
  ),
87
-
88
- exportData: protectedProcedure.query(({ ctx }) =>
89
- exportUserData(ctx.user!.id),
90
- ),
91
-
92
- deleteAccount: protectedProcedure
93
- .input(
94
- z.object({
95
- confirmUsername: z.string().min(1),
96
- }),
97
- )
98
- .mutation(async ({ ctx, input }) => {
99
- const profile = await getProfile(ctx.user!.id);
100
- if (profile.username !== input.confirmUsername) {
101
- throw new TRPCError({
102
- code: "BAD_REQUEST",
103
- message: "Username confirmation does not match",
104
- });
105
- }
106
- return deleteUserAccount(ctx.user!.id);
107
- }),
108
82
  });
@@ -87,13 +87,6 @@ export function applyResolvedFormatsToStreamParts(
87
87
  });
88
88
  }
89
89
 
90
- /** Tools that queue async work after the model stream; UI stays loading until the pipeline finishes. */
91
- const DEFERRED_COMPLETION_TOOLS = new Set(['generate_document']);
92
-
93
- function isDeferredCompletionTool(toolName: string): boolean {
94
- return DEFERRED_COMPLETION_TOOLS.has(toolName);
95
- }
96
-
97
90
  /** Attach file IDs to `generate_document` tool parts for inline download chips. */
98
91
  export function enrichStreamPartsWithGeneratedFiles(
99
92
  parts: StreamPart[],
@@ -133,24 +126,11 @@ export function enrichStreamPartsWithGeneratedFiles(
133
126
  ...part,
134
127
  fileId: file.fileId,
135
128
  fileName: file.fileName,
136
- status: 'completed',
137
129
  ...(file.format ? { format: file.format } : {}),
138
130
  };
139
131
  });
140
132
  }
141
133
 
142
- /** Mark deferred tools completed (file linked) or failed (generation did not produce a file). */
143
- export function finalizeDeferredToolParts(parts: StreamPart[]): StreamPart[] {
144
- return parts.map((part) => {
145
- if (part.type !== 'tool' || !isDeferredCompletionTool(part.toolName)) {
146
- return part;
147
- }
148
- if (part.fileId) return { ...part, status: 'completed' };
149
- if (part.status === 'executing') return { ...part, status: 'failed' };
150
- return part;
151
- });
152
- }
153
-
154
134
  function toolFieldFromInput(input: unknown, key: string): string | undefined {
155
135
  if (input && typeof input === 'object') {
156
136
  const val = (input as Record<string, unknown>)[key];
@@ -291,15 +271,8 @@ export async function streamChatResponse(
291
271
  }
292
272
  case 'tool-result':
293
273
  case 'tool-error': {
294
- const failed = part.type === 'tool-error';
295
- const deferred = isDeferredCompletionTool(part.toolName);
296
- // Deferred tools (e.g. generate_document) only queue work during the
297
- // stream — keep the spinner until the pipeline attaches a file.
298
- const status: 'executing' | 'completed' | 'failed' = failed
299
- ? 'failed'
300
- : deferred
301
- ? 'executing'
302
- : 'completed';
274
+ const status: 'completed' | 'failed' =
275
+ part.type === 'tool-error' ? 'failed' : 'completed';
303
276
  const title = toolTitleFromInput(part.input);
304
277
  const format = toolFieldFromInput(part.input, 'format');
305
278
  const idx = toolPartIndexById.get(part.toolCallId);
@@ -314,7 +287,7 @@ export async function streamChatResponse(
314
287
  toolName: part.toolName,
315
288
  input: part.input,
316
289
  output: part.type === 'tool-result' ? part.output : undefined,
317
- status: failed ? 'failed' : 'completed',
290
+ status,
318
291
  });
319
292
  await broadcast('message-tool-call', {
320
293
  messageId: aiMessageId,
@@ -241,12 +241,6 @@ export function labProposalTools(proposals: LabProposals): ToolSet {
241
241
  inputSchema: jsonSchema({
242
242
  type: 'object',
243
243
  properties: {
244
- title: { type: 'string', description: 'Document title' },
245
- content: {
246
- type: 'string',
247
- description:
248
- 'Full body — markdown or plain text with headings, lists, tables, slide notes, etc.',
249
- },
250
244
  format: {
251
245
  type: 'string',
252
246
  description:
@@ -257,6 +251,11 @@ export function labProposalTools(proposals: LabProposals): ToolSet {
257
251
  'py', 'js', 'ts', 'css', 'sh',
258
252
  ],
259
253
  },
254
+ title: { type: 'string', description: 'Document title' },
255
+ content: {
256
+ type: 'string',
257
+ description: 'Full body — markdown or plain text with headings, lists, tables, slide notes, etc.',
258
+ },
260
259
  },
261
260
  required: ['title', 'content', 'format'],
262
261
  }),
@@ -251,7 +251,7 @@ export function aiTools(classId: string): ToolSet {
251
251
  const client = new OpenAI();
252
252
  const response = await client.responses.create({
253
253
  model: 'gpt-4o-mini',
254
- tools: [{ type: 'web_search' }],
254
+ tools: [{ type: 'web_search_preview' }],
255
255
  input: query,
256
256
  });
257
257
 
@@ -10,7 +10,6 @@ import { sendMail } from "../utils/email.js";
10
10
  import { prismaWrapper } from "../utils/prismaWrapper.js";
11
11
  import { env } from "../lib/config/env.js";
12
12
  import { logger } from "../utils/logger.js";
13
- import { CURRENT_TERMS_VERSION } from "../lib/legal.js";
14
13
  import {
15
14
  findUserByUsername,
16
15
  findUserByUsernameOrEmail,
@@ -33,17 +32,8 @@ export async function register(input: {
33
32
  username: string;
34
33
  email: string;
35
34
  password: string;
36
- agreeToTerms: true;
37
- termsVersion: string;
38
35
  }) {
39
- const { username, email, password, termsVersion } = input;
40
-
41
- if (termsVersion !== CURRENT_TERMS_VERSION) {
42
- throw new TRPCError({
43
- code: "BAD_REQUEST",
44
- message: "Please accept the current Terms of Service and Privacy Policy.",
45
- });
46
- }
36
+ const { username, email, password } = input;
47
37
 
48
38
  const existingUser = await prismaWrapper.findFirst(
49
39
  () => findUserByUsernameOrEmail(username, email),
@@ -80,9 +70,7 @@ export async function register(input: {
80
70
  username,
81
71
  email,
82
72
  password: await hash(password, 10),
83
- verified: false,
84
- termsAcceptedAt: new Date(),
85
- termsVersion,
73
+ verified: true,
86
74
  }),
87
75
  "creating new user during registration"
88
76
  );
@@ -4,7 +4,7 @@
4
4
  import { v4 } from "uuid";
5
5
  import {
6
6
  createSchoolDevelopmentProgram,
7
- findSchoolDevelopmentProgramStatusById,
7
+ findSchoolDevelopmentProgramById,
8
8
  createEarlyAccessRequest,
9
9
  } from "../models/marketing.js";
10
10
 
@@ -42,9 +42,9 @@ export async function createSchoolDevelopmentProgramRecord(input: {
42
42
  return { id: program.id };
43
43
  }
44
44
 
45
- /** Public application status lookup (no contact PII). */
45
+ /** Look up a school development program by ID. */
46
46
  export async function searchSchoolDevelopmentPrograms(id: string) {
47
- return findSchoolDevelopmentProgramStatusById(id);
47
+ return findSchoolDevelopmentProgramById(id);
48
48
  }
49
49
 
50
50
  /** Create an early access request. */