@studious-lms/server 1.8.0 → 1.11.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 (82) hide show
  1. package/dist/middleware/security.d.ts.map +1 -1
  2. package/dist/middleware/security.js +3 -3
  3. package/dist/middleware/security.js.map +1 -1
  4. package/dist/models/agenda.d.ts +2 -2
  5. package/dist/models/announcement.d.ts +2 -2
  6. package/dist/models/assignment.d.ts +33 -33
  7. package/dist/models/class.d.ts +2 -2
  8. package/dist/models/cloudStorage.d.ts +14 -1
  9. package/dist/models/cloudStorage.d.ts.map +1 -1
  10. package/dist/models/cloudStorage.js +123 -3
  11. package/dist/models/cloudStorage.js.map +1 -1
  12. package/dist/models/event.d.ts +3 -3
  13. package/dist/models/file.d.ts +27 -26
  14. package/dist/models/file.d.ts.map +1 -1
  15. package/dist/models/file.js +4 -39
  16. package/dist/models/file.js.map +1 -1
  17. package/dist/models/labChat.d.ts +6 -6
  18. package/dist/models/section.d.ts +1 -1
  19. package/dist/routers/_app.d.ts +444 -100
  20. package/dist/routers/_app.d.ts.map +1 -1
  21. package/dist/routers/_app.js +4 -2
  22. package/dist/routers/_app.js.map +1 -1
  23. package/dist/routers/agenda.d.ts +2 -2
  24. package/dist/routers/assignment.d.ts +31 -31
  25. package/dist/routers/class.d.ts +5 -5
  26. package/dist/routers/event.d.ts +2 -2
  27. package/dist/routers/file.d.ts +2 -2
  28. package/dist/routers/folder.d.ts +24 -0
  29. package/dist/routers/folder.d.ts.map +1 -1
  30. package/dist/routers/folder.js +27 -5
  31. package/dist/routers/folder.js.map +1 -1
  32. package/dist/routers/labChat.d.ts +2 -2
  33. package/dist/routers/newtonChat.d.ts +1 -1
  34. package/dist/routers/permissions.d.ts +120 -0
  35. package/dist/routers/permissions.d.ts.map +1 -0
  36. package/dist/routers/permissions.js +50 -0
  37. package/dist/routers/permissions.js.map +1 -0
  38. package/dist/routers/studentProgress.d.ts +45 -0
  39. package/dist/routers/studentProgress.d.ts.map +1 -1
  40. package/dist/routers/studentProgress.js +12 -3
  41. package/dist/routers/studentProgress.js.map +1 -1
  42. package/dist/routers/tag.d.ts +5 -5
  43. package/dist/services/agenda.d.ts +2 -2
  44. package/dist/services/assignment.d.ts +31 -31
  45. package/dist/services/class.d.ts +5 -5
  46. package/dist/services/class.js +3 -3
  47. package/dist/services/class.js.map +1 -1
  48. package/dist/services/event.d.ts +2 -2
  49. package/dist/services/file.d.ts +22 -5
  50. package/dist/services/file.d.ts.map +1 -1
  51. package/dist/services/file.js +81 -45
  52. package/dist/services/file.js.map +1 -1
  53. package/dist/services/folder.d.ts +97 -2
  54. package/dist/services/folder.d.ts.map +1 -1
  55. package/dist/services/folder.js +158 -87
  56. package/dist/services/folder.js.map +1 -1
  57. package/dist/services/labChat.d.ts +2 -2
  58. package/dist/services/permissions.d.ts +188 -0
  59. package/dist/services/permissions.d.ts.map +1 -0
  60. package/dist/services/permissions.js +661 -0
  61. package/dist/services/permissions.js.map +1 -0
  62. package/dist/services/studentProgress.d.ts +94 -0
  63. package/dist/services/studentProgress.d.ts.map +1 -1
  64. package/dist/services/studentProgress.js +94 -3
  65. package/dist/services/studentProgress.js.map +1 -1
  66. package/package.json +1 -1
  67. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
  68. package/prisma/schema.prisma +74 -0
  69. package/src/middleware/security.ts +1 -1
  70. package/src/models/cloudStorage.ts +134 -1
  71. package/src/models/file.ts +2 -37
  72. package/src/routers/_app.ts +2 -0
  73. package/src/routers/folder.ts +32 -2
  74. package/src/routers/permissions.ts +71 -0
  75. package/src/routers/studentProgress.ts +21 -0
  76. package/src/services/class.ts +1 -1
  77. package/src/services/file.ts +76 -47
  78. package/src/services/folder.ts +180 -91
  79. package/src/services/permissions.ts +778 -0
  80. package/src/services/studentProgress.ts +197 -11
  81. package/tests/unit/services/permissions.test.ts +463 -0
  82. package/tests/unit/services/studentProgress.test.ts +168 -2
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { TRPCError } from "@trpc/server";
5
5
  import type { AssignmentType } from "@prisma/client";
6
+ import { z } from "zod";
6
7
  import { prisma } from "../lib/prisma.js";
7
8
  import { isTeacherInClass } from "../models/class.js";
8
9
  import { inference } from "../utils/inference.js";
@@ -38,10 +39,38 @@ type ProgressStudent = {
38
39
  profile: { displayName: string | null } | null;
39
40
  };
40
41
 
41
- type RecommendationType =
42
- | "missing"
43
- | "low_score"
44
- | "awaiting_feedback";
42
+ const insightItemSchema = z.object({
43
+ label: z.string(),
44
+ evidence: z.string(),
45
+ });
46
+
47
+ const studentProgressInsightsSchema = z.object({
48
+ summary: z.string(),
49
+ strengths: z.array(insightItemSchema).min(2).max(4),
50
+ growth: z.array(insightItemSchema).min(2).max(4),
51
+ talkingPoints: z.array(z.string()).min(3).max(5),
52
+ });
53
+
54
+ const studentProgressForecastSchema = z.object({
55
+ forecastLow: z.number().min(0).max(100),
56
+ forecastHigh: z.number().min(0).max(100),
57
+ risk: z.enum(["on_track", "watch", "at_risk"]),
58
+ reason: z.string(),
59
+ });
60
+
61
+ const studentProgressFocusSchema = z.object({
62
+ focusItems: z.array(z.string()).min(3).max(5),
63
+ });
64
+
65
+ export type StudentProgressInsights = z.infer<
66
+ typeof studentProgressInsightsSchema
67
+ >;
68
+ export type StudentProgressForecast = z.infer<
69
+ typeof studentProgressForecastSchema
70
+ >;
71
+ export type StudentProgressFocus = z.infer<typeof studentProgressFocusSchema>;
72
+
73
+ type RecommendationType = "missing" | "low_score" | "awaiting_feedback";
45
74
 
46
75
  function calculatePercentage(submission: GradeSubmission) {
47
76
  if (submission.gradeReceived == null || !submission.assignment.maxGrade) {
@@ -131,8 +160,7 @@ function buildSuggestedAssignment(
131
160
  percentage != null
132
161
  ? `${displayName} scored ${percentage}% and needs targeted reinforcement on the underlying concepts.`
133
162
  : `${displayName} needs targeted reinforcement on the underlying concepts.`,
134
- awaiting_feedback:
135
- `${displayName} has work in progress here, so a short follow-up can keep momentum once feedback lands.`,
163
+ awaiting_feedback: `${displayName} has work in progress here, so a short follow-up can keep momentum once feedback lands.`,
136
164
  };
137
165
 
138
166
  return {
@@ -429,9 +457,14 @@ export async function getStudentProgressRecommendations(
429
457
  studentId,
430
458
  );
431
459
 
432
- const summary = await summarizeStudentRecommendationsWithCases(student, submissions);
433
- const { recommendationCandidates: _recommendationCandidates, ...summaryResponse } =
434
- summary;
460
+ const summary = await summarizeStudentRecommendationsWithCases(
461
+ student,
462
+ submissions,
463
+ );
464
+ const {
465
+ recommendationCandidates: _recommendationCandidates,
466
+ ...summaryResponse
467
+ } = summary;
435
468
  return {
436
469
  student: summaryResponse.student,
437
470
  summary: summaryResponse.summary,
@@ -498,7 +531,10 @@ export async function getClassProgressRecommendations(
498
531
  ),
499
532
  )
500
533
  )
501
- .map(({ recommendationCandidates: _recommendationCandidates, ...student }) => student)
534
+ .map(
535
+ ({ recommendationCandidates: _recommendationCandidates, ...student }) =>
536
+ student,
537
+ )
502
538
  .filter((student) => student.recommendations.length > 0)
503
539
  .sort((a, b) => b.priorityScore - a.priorityScore);
504
540
 
@@ -591,6 +627,152 @@ function buildProgressSummary(submissions: GradeSubmission[]) {
591
627
  }));
592
628
  }
593
629
 
630
+ function buildStructuredProgressContext(
631
+ classData: { id: string; name: string; subject: string | null },
632
+ student: ProgressStudent,
633
+ submissions: GradeSubmission[],
634
+ ) {
635
+ const displayName = student.profile?.displayName ?? student.username;
636
+ const recommendationSummary = summarizeStudentRecommendations(
637
+ student,
638
+ submissions,
639
+ );
640
+
641
+ return {
642
+ class: classData,
643
+ student: { id: student.id, username: student.username, displayName },
644
+ progress: {
645
+ overallGrade: getOverallGrade(submissions),
646
+ trend: calculateTrend(submissions),
647
+ summary: recommendationSummary.summary,
648
+ assignments: buildProgressSummary(submissions),
649
+ recommendations: recommendationSummary.recommendations.map(
650
+ (recommendation) => ({
651
+ title: recommendation.title,
652
+ recommendationType: recommendation.recommendationType,
653
+ percentage: recommendation.percentage,
654
+ submitted: recommendation.submitted,
655
+ reasons: recommendation.reasons,
656
+ sectionName: recommendation.sectionName,
657
+ }),
658
+ ),
659
+ },
660
+ };
661
+ }
662
+
663
+ function buildStructuredInferenceMessages(
664
+ instruction: string,
665
+ context: ReturnType<typeof buildStructuredProgressContext>,
666
+ ) {
667
+ return [
668
+ {
669
+ role: "system" as const,
670
+ content:
671
+ "You are an educational progress analyst. Use only the provided class and grade context. Be specific, concise, supportive, and do not fabricate assignments, grades, or sections.",
672
+ },
673
+ {
674
+ role: "user" as const,
675
+ content: JSON.stringify(context),
676
+ },
677
+ {
678
+ role: "user" as const,
679
+ content: instruction,
680
+ },
681
+ ];
682
+ }
683
+
684
+ export async function getStudentProgressInsights(
685
+ viewerId: string,
686
+ classId: string,
687
+ studentId: string,
688
+ ) {
689
+ const { classData, student, submissions } = await loadStudentProgressContext(
690
+ viewerId,
691
+ classId,
692
+ studentId,
693
+ );
694
+ const context = buildStructuredProgressContext(
695
+ classData,
696
+ student,
697
+ submissions,
698
+ );
699
+
700
+ return inference<StudentProgressInsights>(
701
+ buildStructuredInferenceMessages(
702
+ [
703
+ "Generate an AI analytics summary for this one student's dashboard.",
704
+ "The summary must be 2-4 plain-prose sentences: lead with overall grade and trajectory, then call out one strength and one concern with assignment titles.",
705
+ "Strengths and growth items need 2-6 word labels and one-line evidence grounded in assignment titles or sections.",
706
+ "Talking points must be 3-5 specific teacher check-in prompts with no numbering.",
707
+ ].join(" "),
708
+ context,
709
+ ),
710
+ studentProgressInsightsSchema,
711
+ {
712
+ reason: "student_progress_insights",
713
+ metadata: { viewerId, classId, studentId },
714
+ },
715
+ );
716
+ }
717
+
718
+ export async function getStudentProgressForecast(
719
+ viewerId: string,
720
+ classId: string,
721
+ studentId: string,
722
+ ) {
723
+ const { classData, student, submissions } = await loadStudentProgressContext(
724
+ viewerId,
725
+ classId,
726
+ studentId,
727
+ );
728
+ const context = buildStructuredProgressContext(
729
+ classData,
730
+ student,
731
+ submissions,
732
+ );
733
+
734
+ return inference<StudentProgressForecast>(
735
+ buildStructuredInferenceMessages(
736
+ "Predict the student's next graded assignment percentage band and risk status. Base the forecast on trajectory, missing work, low scores, and assignment recency. Keep the reason to one sentence.",
737
+ context,
738
+ ),
739
+ studentProgressForecastSchema,
740
+ {
741
+ reason: "student_progress_forecast",
742
+ metadata: { viewerId, classId, studentId },
743
+ },
744
+ );
745
+ }
746
+
747
+ export async function getStudentProgressFocus(
748
+ viewerId: string,
749
+ classId: string,
750
+ studentId: string,
751
+ ) {
752
+ const { classData, student, submissions } = await loadStudentProgressContext(
753
+ viewerId,
754
+ classId,
755
+ studentId,
756
+ );
757
+ const context = buildStructuredProgressContext(
758
+ classData,
759
+ student,
760
+ submissions,
761
+ );
762
+
763
+ return inference<StudentProgressFocus>(
764
+ buildStructuredInferenceMessages(
765
+ "Generate 3-5 student-facing weekly focus items. Each item should be specific, actionable, and grounded in the student's grade pattern or assignment titles. No numbering in the strings.",
766
+ context,
767
+ ),
768
+ studentProgressFocusSchema,
769
+ {
770
+ reason: "student_progress_focus",
771
+ metadata: { viewerId, classId, studentId },
772
+ },
773
+ );
774
+ }
775
+
594
776
  export async function chatAboutStudentProgress(
595
777
  viewerId: string,
596
778
  input: {
@@ -637,7 +819,11 @@ export async function chatAboutStudentProgress(
637
819
  try {
638
820
  const response = await inference<string>(messages, undefined, {
639
821
  reason: "student_progress_chat",
640
- metadata: { viewerId, classId: input.classId, studentId: input.studentId },
822
+ metadata: {
823
+ viewerId,
824
+ classId: input.classId,
825
+ studentId: input.studentId,
826
+ },
641
827
  });
642
828
  if (typeof response !== "string" || response.trim().length === 0) {
643
829
  throw new Error("Student progress chat returned an empty response");