@brandon_m_behring/book-scaffold-astro 3.6.5 → 3.7.1

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/src/schemas.ts ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Zod schemas + enum constants for book content collections.
3
+ *
4
+ * All Zod schemas live in this single file (single `astro/zod` import) so
5
+ * tsup's DTS bundler doesn't traverse Zod's dual CJS/ESM package multiple
6
+ * times — rollup-plugin-dts can't resolve Zod v4's `default` export when
7
+ * the same Zod import appears in multiple entry-graph files.
8
+ *
9
+ * Per-profile organization lives at src/profiles/<name>.ts which imports
10
+ * these schemas as values + declares the inferred chapter type + the
11
+ * route/style defaults. See ~/.claude/plans/address-and-finish-moonlit-shell.md.
12
+ *
13
+ * Imports `z` from `astro/zod` (a real module re-export) so schemas can
14
+ * be constructed at package-load time outside an Astro runtime context.
15
+ * `defineBookSchemas` in schemas-entry.ts wraps these into Astro
16
+ * `defineCollection` calls at the consumer's content-config load time.
17
+ */
18
+ import { z } from 'astro/zod';
19
+
20
+ // ===== Tools-profile enums =====
21
+
22
+ export const toolSlugs = [
23
+ 'claude-code',
24
+ 'gemini-cli',
25
+ 'codex-cli',
26
+ 'cross-tool',
27
+ ] as const;
28
+
29
+ export const volatilityLevels = [
30
+ 'stable-principle',
31
+ 'architectural-pattern',
32
+ 'feature-surface',
33
+ ] as const;
34
+
35
+ export const sourceTiers = [
36
+ 'T1-official',
37
+ 'T2-release-notes',
38
+ 'T3-practitioner',
39
+ 'T4-conjecture',
40
+ ] as const;
41
+
42
+ export const changeKinds = ['added', 'removed', 'changed', 'deprecated'] as const;
43
+
44
+ export const patternCategories = [
45
+ 'safety',
46
+ 'scale',
47
+ 'context',
48
+ 'interaction',
49
+ 'extension',
50
+ 'other',
51
+ ] as const;
52
+
53
+ // ===== Academic-profile enums =====
54
+
55
+ export const academicParts = [
56
+ 'foundations',
57
+ 'ssm-core',
58
+ 'beyond-ssm',
59
+ 'integration',
60
+ 'synthesis',
61
+ ] as const;
62
+
63
+ export const chapterStatus = [
64
+ 'implemented',
65
+ 'chapter_only',
66
+ 'reading_only',
67
+ 'prose_only',
68
+ 'code_only',
69
+ 'scaffolded',
70
+ 'planned',
71
+ ] as const;
72
+
73
+ // ===== Chapter schemas — one per profile =====
74
+
75
+ export const academicChapterSchema = z.object({
76
+ week: z.number().int().min(1).max(99),
77
+ part: z.enum(academicParts),
78
+ title: z.string().min(1),
79
+ status: z.enum(chapterStatus),
80
+ roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
81
+ code_path: z.string().optional(),
82
+ tests_path: z.string().optional(),
83
+ notebook_path: z.string().optional(),
84
+ description: z.string().optional(),
85
+ draft: z.boolean().default(false),
86
+ });
87
+
88
+ export const toolsChapterSchema = z.object({
89
+ title: z.string().min(1),
90
+ part: z.number().int().min(0).max(10),
91
+ chapter: z.number().int().min(0).max(99),
92
+ volatility: z.enum(volatilityLevels),
93
+ tools_compared: z.array(z.enum(toolSlugs)).min(1),
94
+ last_verified: z.date(),
95
+ sources: z.array(z.string()).default([]),
96
+ description: z.string().optional(),
97
+ draft: z.boolean().default(false),
98
+ updated: z.date().optional(),
99
+ });
100
+
101
+ /** Minimal profile currently aliases the tools schema. */
102
+ export const minimalChapterSchema = toolsChapterSchema;
103
+
104
+ /**
105
+ * Research-portfolio source tiers (v3.5.0, closes issue #6).
106
+ *
107
+ * Lighter shape than the tools-profile `sourceTiers` enum (`'T1-official'` etc.)
108
+ * — research portfolios cite primary sources inline per-chapter, so short
109
+ * `'T1'`/`'T2'` is more compact and readable. Semantics overlap (T1 = official
110
+ * primary, T2 = secondary, T3 = practitioner / community, T4 = conjecture).
111
+ */
112
+ export const sourceTiersResearch = ['T1', 'T2', 'T3', 'T4'] as const;
113
+
114
+ /**
115
+ * Course-notes profile schema (v3.3.0, closes issue #4). Designed for
116
+ * course-derived study notes (DLAI, Coursera, Manning, ...). Key fields:
117
+ * - `course`/`instructor`/`source_url` — attribution
118
+ * - `learning_outcomes` — structured Bloom-tag-ready outcomes
119
+ * - `tags` — freeform string array (NOT tools_compared enum)
120
+ */
121
+ export const courseNotesChapterSchema = z.object({
122
+ // Identity
123
+ title: z.string().min(1),
124
+ chapter: z.number().int().min(0).max(99),
125
+ part: z.number().int().min(0).max(20).default(1),
126
+ description: z.string().optional(),
127
+
128
+ // Source attribution
129
+ course: z.string().optional(),
130
+ instructor: z.string().optional(),
131
+ source_url: z.string().url().optional(),
132
+
133
+ // Pedagogy
134
+ learning_outcomes: z
135
+ .array(
136
+ z.object({
137
+ id: z.string(),
138
+ verb: z.string(),
139
+ text: z.string(),
140
+ }),
141
+ )
142
+ .default([]),
143
+ tags: z.array(z.string()).default([]),
144
+
145
+ // Provenance + status (shared shape with tools profile)
146
+ last_verified: z.date(),
147
+ volatility: z.enum(volatilityLevels).default('architectural-pattern'),
148
+ sources: z.array(z.string()).default([]),
149
+ draft: z.boolean().default(false),
150
+ });
151
+
152
+ /**
153
+ * Research-portfolio profile schema (v3.5.0, closes issue #6).
154
+ *
155
+ * Union of academic + tools field shapes, modernized: uses `tags` (freeform
156
+ * string array) instead of `tools_compared` (CLI-enum, doesn't fit research
157
+ * content). Designed for research-portfolio books that need BOTH academic-
158
+ * style structure (week/part/status, math/BibTeX/Theorem support via the
159
+ * `katex: true` profile flag) AND tools-style provenance (volatility class,
160
+ * tier-tagged sources, last_verified freshness signal).
161
+ *
162
+ * Reference (forthcoming) consumer: prompt-injection-portfolio.
163
+ *
164
+ * Hierarchy fields are all optional — chapters can use academic-style
165
+ * (`week` + part-enum string) OR tools-style (`chapter` + part-number) OR
166
+ * minimal (just title). The route templates dispatch on which is set.
167
+ *
168
+ * Sources are STRUCTURED INLINE (each chapter cites primary sources directly)
169
+ * rather than referencing a sources collection — saves cross-file lookup +
170
+ * matches research-paper citation conventions. Tier shorthand T1/T2/T3/T4
171
+ * (per sourceTiersResearch) over the tools-profile long form.
172
+ */
173
+ export const researchPortfolioChapterSchema = z.object({
174
+ // Identity
175
+ title: z.string().min(1),
176
+ slug: z.string().optional(), // explicit slug override (otherwise filename)
177
+ description: z.string().optional(),
178
+
179
+ // Hierarchy — accept either academic-style or tools-style; all optional.
180
+ // The academic 'part' field is a string enum; tools 'part' is a number.
181
+ // Use z.union to permit either type.
182
+ part: z.union([z.number().int().min(0).max(20), z.string()]).optional(),
183
+ week: z.number().int().min(0).max(99).optional(),
184
+ chapter: z.number().int().min(0).max(99).optional(),
185
+
186
+ // Academic-style status (optional for research-portfolio — books may track
187
+ // chapters as 'prose_only' / 'experimental-result' / etc.).
188
+ status: z
189
+ .enum([
190
+ 'implemented',
191
+ 'chapter_only',
192
+ 'reading_only',
193
+ 'prose_only',
194
+ 'code_only',
195
+ 'scaffolded',
196
+ 'planned',
197
+ ])
198
+ .optional(),
199
+
200
+ // Research-portfolio specific: nature of the chapter's content.
201
+ // Distinct from academic's 'status' (which tracks authoring state) — this
202
+ // describes the EVIDENCE TYPE the chapter rests on.
203
+ freshness: z
204
+ .enum([
205
+ 'experimental-result', // primary data the author produced
206
+ 'literature-survey', // synthesis of others' work
207
+ 'theoretical', // analytical / mathematical argument
208
+ 'reference', // canonical material (definitions, taxonomy)
209
+ ])
210
+ .optional(),
211
+
212
+ // Provenance (tools-style — overlap with tools/course-notes profiles).
213
+ volatility: z.enum(volatilityLevels).optional(),
214
+ tags: z.array(z.string()).default([]), // freeform; replaces tools_compared
215
+
216
+ // Structured inline sources with T1-T4 tiers.
217
+ sources: z
218
+ .array(
219
+ z.object({
220
+ tier: z.enum(sourceTiersResearch),
221
+ url: z.string().url(),
222
+ label: z.string().min(1),
223
+ }),
224
+ )
225
+ .default([]),
226
+
227
+ // Status + dates.
228
+ last_verified: z.date(),
229
+ updated: z.date().optional(),
230
+ draft: z.boolean().default(false),
231
+ });
232
+
233
+ // ===== Inferred chapter types — one per schema =====
234
+ //
235
+ // Exported here so per-profile modules can re-export under a common name
236
+ // (AcademicChapter, ToolsChapter, etc.) without each touching `z.infer`
237
+ // in its own file (which would multiply the Zod import points and trip
238
+ // rollup-plugin-dts).
239
+
240
+ export type AcademicChapter = z.infer<typeof academicChapterSchema>;
241
+ export type ToolsChapter = z.infer<typeof toolsChapterSchema>;
242
+ export type MinimalChapter = z.infer<typeof minimalChapterSchema>;
243
+ export type CourseNotesChapter = z.infer<typeof courseNotesChapterSchema>;
244
+ export type ResearchPortfolioChapter = z.infer<typeof researchPortfolioChapterSchema>;
245
+
246
+ // ===== Collateral collection schemas (tools-profile; always-defined) =====
247
+
248
+ export const sourcesSchema = z.object({
249
+ url: z.string().url(),
250
+ title: z.string().min(1),
251
+ author: z.string().optional(),
252
+ publish_date: z.date().optional(),
253
+ captured_at: z.date(),
254
+ content_hash: z
255
+ .string()
256
+ .regex(/^sha256:[a-f0-9]+$/)
257
+ .optional(),
258
+ tier: z.enum(sourceTiers),
259
+ tool: z.enum(toolSlugs),
260
+ perma_cc: z.string().url().nullable().optional(),
261
+ local_cache: z.string().nullable().optional(),
262
+ });
263
+
264
+ export const changelogSchema = z.object({
265
+ tool: z.enum(toolSlugs),
266
+ versions: z
267
+ .array(
268
+ z.object({
269
+ version: z.string().min(1),
270
+ date: z.date(),
271
+ changes: z
272
+ .array(
273
+ z.object({
274
+ pattern: z.string(),
275
+ kind: z.enum(changeKinds),
276
+ note: z.string().min(1),
277
+ source_key: z.string().optional(),
278
+ }),
279
+ )
280
+ .default([]),
281
+ }),
282
+ )
283
+ .default([]),
284
+ });
285
+
286
+ export const patternsSchema = z.object({
287
+ name: z.string().min(1),
288
+ description: z.string().optional(),
289
+ category: z.enum(patternCategories).optional(),
290
+ convergence_date: z.date().nullable().optional(),
291
+ });