Files
website/src/lib/server/db/schema/math.ts
Stan Runge d99245e9fc
Some checks failed
Deploy production / deploy (push) Failing after 9s
Math tracker (/trackers/math): Evan Chen's Napkin, chapter by chapter
Schema math (parts, chapters, sections, activity), seeded with all 100 chapters
(Ch 0-94, appendix A-E), 647 sections, PDF pages, exercise counts and approximate
prereqs. Triggers stamp started_at/completed_at and log status/exercise/note/section
changes; math.apply_progress(jsonb) applies the JSON format bots send. Private by
default via MATH_PRIVATE in #lib/math (owner-only prefix + lock in every tracker list).
Overall + per-part progress, current chapter, prereq hints, quick status buttons,
exercises stepper, notes, optional section ticks, activity log, dependency chart.
Deploy preflight checks the math schema (4 tables, 100 chapters).
2026-10-26 10:55:16 +00:00

113 lines
4.0 KiB
TypeScript

import { sql } from 'drizzle-orm';
import { check, index, integer, text, uniqueIndex } from 'drizzle-orm/pg-core';
import { mathSchema } from './schemas';
import { id, timestamps, timestamptz } from './helpers';
// Math tracker (/trackers/math): working through Evan Chen's "An Infinitely Large Napkin".
// Parts and chapters (with sections) are seeded from the book's outline in the migration.
// Progress lives on the chapter rows; triggers (in the migration) keep started_at/completed_at
// and log every status / exercise / note / section change to `activity`. Bots and scripts
// update progress with `select math.apply_progress('{"chapter":"3","status":"done"}')` or a
// plain UPDATE (both are logged). Prereqs are hints only, never locks.
// Statuses (kept in sync with MATH_STATUSES in #lib/math; the schema folder must not import
// from outside it): not_started, reading, exercises, done, skipped.
export const mathParts = mathSchema.table(
'parts',
{
id: id(),
/** "1".."22" or "appendix". */
key: text('key').notNull(),
/** Part number; null for the appendix. */
num: integer('num'),
title: text('title').notNull(),
sortOrder: integer('sort_order').notNull()
},
(t) => [uniqueIndex('parts_key_key').on(t.key)]
);
export const mathChapters = mathSchema.table(
'chapters',
{
id: id(),
/** Chapter number as text ("0".."94") or the appendix letter ("A".."E"). */
key: text('key').notNull(),
partId: integer('part_id')
.notNull()
.references(() => mathParts.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
title: text('title').notNull(),
/** PDF pages of the free Napkin.pdf (link with #page=N). */
pageStart: integer('page_start').notNull(),
pageEnd: integer('page_end').notNull(),
/** Problems at the end of the chapter; null when the chapter has none. */
exerciseCount: integer('exercise_count'),
/** Chapter keys this one builds on (approximate, from the book's dependency chart). */
prereqs: text('prereqs')
.array()
.notNull()
.default(sql`'{}'::text[]`),
status: text('status').notNull().default('not_started'),
exercisesDone: integer('exercises_done').notNull().default(0),
notes: text('notes'),
startedAt: timestamptz('started_at'),
completedAt: timestamptz('completed_at'),
sortOrder: integer('sort_order').notNull(),
...timestamps
},
(t) => [
uniqueIndex('chapters_key_key').on(t.key),
index('chapters_part_idx').on(t.partId, t.sortOrder),
check(
'chapters_status_check',
sql`${t.status} IN ('not_started','reading','exercises','done','skipped')`
),
check('chapters_exercises_done_check', sql`${t.exercisesDone} >= 0`)
]
);
export const mathSections = mathSchema.table(
'sections',
{
id: id(),
chapterId: integer('chapter_id')
.notNull()
.references(() => mathChapters.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
/** Section number, e.g. "3.2" (or "D.1"). */
key: text('key').notNull(),
title: text('title').notNull(),
sortOrder: integer('sort_order').notNull(),
/** Optional per-section tick. */
doneAt: timestamptz('done_at')
},
(t) => [uniqueIndex('sections_chapter_key_key').on(t.chapterId, t.key)]
);
export const mathActivity = mathSchema.table(
'activity',
{
id: id(),
chapterId: integer('chapter_id')
.notNull()
.references(() => mathChapters.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
sectionId: integer('section_id').references(() => mathSections.id, {
onDelete: 'set null',
onUpdate: 'cascade'
}),
/** status | exercises | note | section */
kind: text('kind').notNull(),
fromValue: text('from_value'),
toValue: text('to_value'),
/** exercises: done count change. */
delta: integer('delta'),
/** site | bot | sql (from the math.source setting, see math.apply_progress). */
source: text('source').notNull().default('sql'),
createdAt: timestamptz('created_at').notNull().defaultNow()
},
(t) => [
index('activity_created_idx').on(t.createdAt),
index('activity_chapter_idx').on(t.chapterId, t.createdAt),
check('activity_kind_check', sql`${t.kind} IN ('status','exercises','note','section')`)
]
);