Skip to content

Commit 283f5b1

Browse files
authored
Merge pull request #33 from willwearing/fix/vercel-build-errors
fix: Vercel build — inline backend schemas, fix TS types
2 parents ee292fc + 4e40627 commit 283f5b1

4 files changed

Lines changed: 278 additions & 20 deletions

File tree

backend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"lint": "eslint \"{src,test}/**/*.ts\" --fix"
1313
},
1414
"dependencies": {
15-
"@graspful/shared": "workspace:*",
1615
"@nestjs/common": "^10.4.0",
1716
"@nestjs/config": "^3.3.0",
1817
"@nestjs/core": "^10.4.0",

backend/src/knowledge-graph/review.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class ReviewService {
112112
for (const concept of courseYaml.concepts) {
113113
adjList.set(
114114
concept.id,
115-
concept.prerequisites.filter((p) => conceptIds.has(p)),
115+
concept.prerequisites.filter((p: string) => conceptIds.has(p)),
116116
);
117117
}
118118

@@ -141,7 +141,7 @@ export class ReviewService {
141141
inStack.delete(node);
142142
}
143143

144-
for (const id of conceptIds) {
144+
for (const id of conceptIds as Set<string>) {
145145
if (!visited.has(id)) dfs(id);
146146
if (hasCycle) break;
147147
}
Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,75 @@
1-
export {
2-
AcademyManifestSchema,
3-
type AcademyManifest,
4-
type AcademyManifestPart,
5-
type AcademyManifestCourse,
6-
} from '@graspful/shared';
1+
import { z } from 'zod';
2+
3+
const AcademyMetaSchema = z.object({
4+
id: z.string().min(1),
5+
name: z.string().min(1),
6+
description: z.string().optional(),
7+
version: z.string().min(1),
8+
});
9+
10+
const AcademyPartSchema = z.object({
11+
id: z.string().min(1),
12+
name: z.string().min(1),
13+
description: z.string().optional(),
14+
});
15+
16+
const AcademyCourseEntrySchema = z.object({
17+
id: z.string().min(1),
18+
name: z.string().min(1),
19+
description: z.string().optional(),
20+
part: z.string().min(1).optional(),
21+
file: z.string().min(1),
22+
});
23+
24+
export const AcademyManifestSchema = z
25+
.object({
26+
academy: AcademyMetaSchema,
27+
parts: z.array(AcademyPartSchema).default([]),
28+
courses: z.array(AcademyCourseEntrySchema).min(1),
29+
})
30+
.superRefine((data, ctx) => {
31+
const partIds = new Set<string>();
32+
for (const part of data.parts) {
33+
if (partIds.has(part.id)) {
34+
ctx.addIssue({
35+
code: z.ZodIssueCode.custom,
36+
message: `Duplicate academy part "${part.id}"`,
37+
});
38+
continue;
39+
}
40+
partIds.add(part.id);
41+
}
42+
43+
const courseIds = new Set<string>();
44+
const courseFiles = new Set<string>();
45+
for (const course of data.courses) {
46+
if (courseIds.has(course.id)) {
47+
ctx.addIssue({
48+
code: z.ZodIssueCode.custom,
49+
message: `Duplicate academy course "${course.id}"`,
50+
});
51+
} else {
52+
courseIds.add(course.id);
53+
}
54+
55+
if (courseFiles.has(course.file)) {
56+
ctx.addIssue({
57+
code: z.ZodIssueCode.custom,
58+
message: `Duplicate academy course file "${course.file}"`,
59+
});
60+
} else {
61+
courseFiles.add(course.file);
62+
}
63+
64+
if (course.part && !partIds.has(course.part)) {
65+
ctx.addIssue({
66+
code: z.ZodIssueCode.custom,
67+
message: `Course "${course.id}" references unknown academy part "${course.part}"`,
68+
});
69+
}
70+
}
71+
});
72+
73+
export type AcademyManifest = z.infer<typeof AcademyManifestSchema>;
74+
export type AcademyManifestPart = z.infer<typeof AcademyPartSchema>;
75+
export type AcademyManifestCourse = z.infer<typeof AcademyCourseEntrySchema>;
Lines changed: 201 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,201 @@
1-
export {
2-
CourseYamlSchema,
3-
type CourseYaml,
4-
type SectionYaml,
5-
type SectionExamYaml,
6-
type SectionExamBlueprintYaml,
7-
type ConceptYaml,
8-
type KnowledgePointYaml,
9-
type ProblemYaml,
10-
type ContentBlockYaml,
11-
} from '@graspful/shared';
1+
import { z } from 'zod';
2+
3+
const ContentBlockYamlSchema = z.discriminatedUnion('type', [
4+
z.object({
5+
type: z.literal('image'),
6+
url: z.string().min(1),
7+
alt: z.string().min(1),
8+
caption: z.string().optional(),
9+
width: z.number().int().positive().optional(),
10+
}),
11+
z.object({
12+
type: z.literal('video'),
13+
url: z.string().min(1),
14+
title: z.string().min(1),
15+
caption: z.string().optional(),
16+
}),
17+
z.object({
18+
type: z.literal('link'),
19+
url: z.string().min(1),
20+
title: z.string().min(1),
21+
description: z.string().optional(),
22+
}),
23+
z.object({
24+
type: z.literal('callout'),
25+
title: z.string().min(1),
26+
body: z.string().min(1),
27+
}),
28+
]);
29+
30+
const ProblemYamlSchema = z.object({
31+
id: z.string(),
32+
type: z.enum([
33+
'multiple_choice',
34+
'fill_blank',
35+
'true_false',
36+
'ordering',
37+
'matching',
38+
'scenario',
39+
]),
40+
question: z.string(),
41+
options: z.array(z.string()).optional(),
42+
correct: z.union([z.number(), z.string()]),
43+
explanation: z.string().optional(),
44+
difficulty: z.number().int().min(1).max(5).optional(),
45+
});
46+
47+
const KnowledgePointYamlSchema = z.object({
48+
id: z.string(),
49+
instruction: z.string().optional(),
50+
instructionContent: z.array(ContentBlockYamlSchema).optional().default([]),
51+
workedExample: z.string().optional(),
52+
workedExampleContent: z.array(ContentBlockYamlSchema).optional().default([]),
53+
problems: z.array(ProblemYamlSchema).optional().default([]),
54+
});
55+
56+
const EncompassingRefSchema = z.object({
57+
concept: z.string(),
58+
weight: z.number().min(0).max(1),
59+
});
60+
61+
const ConceptYamlSchema = z.object({
62+
id: z.string(),
63+
name: z.string(),
64+
section: z.string().optional(),
65+
difficulty: z.number().int().min(1).max(10),
66+
estimatedMinutes: z.number().int().positive(),
67+
tags: z.array(z.string()).default([]),
68+
sourceRef: z.string().optional(),
69+
prerequisites: z.array(z.string()).optional().default([]),
70+
encompassing: z.array(EncompassingRefSchema).optional().default([]),
71+
knowledgePoints: z.array(KnowledgePointYamlSchema).optional().default([]),
72+
});
73+
74+
const SectionExamBlueprintYamlSchema = z.object({
75+
conceptId: z.string(),
76+
minQuestions: z.number().int().positive(),
77+
});
78+
79+
const SectionExamYamlSchema = z.object({
80+
enabled: z.boolean().default(true),
81+
passingScore: z.number().min(0).max(1).default(0.75),
82+
timeLimitMinutes: z.number().int().positive().optional(),
83+
questionCount: z.number().int().positive().default(10),
84+
blueprint: z.array(SectionExamBlueprintYamlSchema).default([]),
85+
instructions: z.string().optional(),
86+
});
87+
88+
const SectionYamlSchema = z.object({
89+
id: z.string(),
90+
name: z.string(),
91+
description: z.string().optional(),
92+
sectionExam: SectionExamYamlSchema.optional(),
93+
});
94+
95+
const CourseMetaSchema = z.object({
96+
id: z.string(),
97+
name: z.string(),
98+
description: z.string().optional(),
99+
estimatedHours: z.number().positive(),
100+
version: z.string(),
101+
sourceDocument: z.string().optional(),
102+
});
103+
104+
export const CourseYamlSchema = z
105+
.object({
106+
course: CourseMetaSchema,
107+
sections: z.array(SectionYamlSchema).optional().default([]),
108+
concepts: z.array(ConceptYamlSchema),
109+
})
110+
.superRefine((data, ctx) => {
111+
const conceptIds = new Set(data.concepts.map((concept) => concept.id));
112+
const sectionIds = new Set(data.sections.map((section) => section.id));
113+
const conceptToSection = new Map(
114+
data.concepts
115+
.filter((concept) => concept.section)
116+
.map((concept) => [concept.id, concept.section as string]),
117+
);
118+
119+
for (const concept of data.concepts) {
120+
if (concept.section && !sectionIds.has(concept.section)) {
121+
ctx.addIssue({
122+
code: z.ZodIssueCode.custom,
123+
message: `Concept "${concept.id}" references unknown section "${concept.section}"`,
124+
});
125+
}
126+
}
127+
128+
for (const section of data.sections) {
129+
const exam = section.sectionExam;
130+
if (!exam?.enabled) {
131+
continue;
132+
}
133+
134+
const sectionConcepts = data.concepts.filter(
135+
(concept) => concept.section === section.id,
136+
);
137+
138+
if (sectionConcepts.length < 2) {
139+
ctx.addIssue({
140+
code: z.ZodIssueCode.custom,
141+
message: `Section "${section.id}" needs at least two concepts when sectionExam is enabled`,
142+
});
143+
}
144+
145+
const minQuestions = exam.blueprint.reduce(
146+
(sum, item) => sum + item.minQuestions,
147+
0,
148+
);
149+
if (exam.questionCount < minQuestions) {
150+
ctx.addIssue({
151+
code: z.ZodIssueCode.custom,
152+
message: `Section "${section.id}" questionCount must be at least the sum of blueprint minQuestions`,
153+
});
154+
}
155+
156+
const availableProblems = sectionConcepts.reduce(
157+
(sum, concept) =>
158+
sum +
159+
concept.knowledgePoints.reduce(
160+
(kpSum, kp) => kpSum + kp.problems.length,
161+
0,
162+
),
163+
0,
164+
);
165+
166+
if (availableProblems < exam.questionCount) {
167+
ctx.addIssue({
168+
code: z.ZodIssueCode.custom,
169+
message: `Section "${section.id}" does not have enough eligible problems for its section exam`,
170+
});
171+
}
172+
173+
for (const item of exam.blueprint) {
174+
if (!conceptIds.has(item.conceptId)) {
175+
ctx.addIssue({
176+
code: z.ZodIssueCode.custom,
177+
message: `Section "${section.id}" blueprint references unknown concept "${item.conceptId}"`,
178+
});
179+
continue;
180+
}
181+
182+
if (conceptToSection.get(item.conceptId) !== section.id) {
183+
ctx.addIssue({
184+
code: z.ZodIssueCode.custom,
185+
message: `Section "${section.id}" blueprint concept "${item.conceptId}" must belong to the same section`,
186+
});
187+
}
188+
}
189+
}
190+
});
191+
192+
export type CourseYaml = z.infer<typeof CourseYamlSchema>;
193+
export type SectionYaml = z.infer<typeof SectionYamlSchema>;
194+
export type SectionExamYaml = z.infer<typeof SectionExamYamlSchema>;
195+
export type SectionExamBlueprintYaml = z.infer<
196+
typeof SectionExamBlueprintYamlSchema
197+
>;
198+
export type ConceptYaml = z.infer<typeof ConceptYamlSchema>;
199+
export type KnowledgePointYaml = z.infer<typeof KnowledgePointYamlSchema>;
200+
export type ProblemYaml = z.infer<typeof ProblemYamlSchema>;
201+
export type ContentBlockYaml = z.infer<typeof ContentBlockYamlSchema>;

0 commit comments

Comments
 (0)