Math tracker (/trackers/math): Evan Chen's Napkin, chapter by chapter
Some checks failed
Deploy production / deploy (push) Failing after 9s

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).
This commit is contained in:
2026-10-26 10:55:16 +00:00
parent 23a43049a0
commit d99245e9fc
16 changed files with 45741 additions and 4 deletions

View File

@@ -35,7 +35,8 @@ export default defineConfig({
'ai', 'ai',
'movies', 'movies',
'worldtodo', 'worldtodo',
'notes' 'notes',
'math'
], ],
migrations: { schema: 'drizzle', table: '__drizzle_migrations' }, migrations: { schema: 'drizzle', table: '__drizzle_migrations' },
verbose: true, verbose: true,

View File

@@ -0,0 +1,983 @@
CREATE SCHEMA "math";
--> statement-breakpoint
CREATE TABLE "math"."activity" (
"id" serial PRIMARY KEY,
"chapter_id" integer NOT NULL,
"section_id" integer,
"kind" text NOT NULL,
"from_value" text,
"to_value" text,
"delta" integer,
"source" text DEFAULT 'sql' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "activity_kind_check" CHECK ("kind" IN ('status','exercises','note','section'))
);
--> statement-breakpoint
CREATE TABLE "math"."chapters" (
"id" serial PRIMARY KEY,
"key" text NOT NULL,
"part_id" integer NOT NULL,
"title" text NOT NULL,
"page_start" integer NOT NULL,
"page_end" integer NOT NULL,
"exercise_count" integer,
"prereqs" text[] DEFAULT '{}'::text[] NOT NULL,
"status" text DEFAULT 'not_started' NOT NULL,
"exercises_done" integer DEFAULT 0 NOT NULL,
"notes" text,
"started_at" timestamp with time zone,
"completed_at" timestamp with time zone,
"sort_order" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "chapters_status_check" CHECK ("status" IN ('not_started','reading','exercises','done','skipped')),
CONSTRAINT "chapters_exercises_done_check" CHECK ("exercises_done" >= 0)
);
--> statement-breakpoint
CREATE TABLE "math"."parts" (
"id" serial PRIMARY KEY,
"key" text NOT NULL,
"num" integer,
"title" text NOT NULL,
"sort_order" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "math"."sections" (
"id" serial PRIMARY KEY,
"chapter_id" integer NOT NULL,
"key" text NOT NULL,
"title" text NOT NULL,
"sort_order" integer NOT NULL,
"done_at" timestamp with time zone
);
--> statement-breakpoint
CREATE INDEX "activity_created_idx" ON "math"."activity" ("created_at");--> statement-breakpoint
CREATE INDEX "activity_chapter_idx" ON "math"."activity" ("chapter_id","created_at");--> statement-breakpoint
CREATE UNIQUE INDEX "chapters_key_key" ON "math"."chapters" ("key");--> statement-breakpoint
CREATE INDEX "chapters_part_idx" ON "math"."chapters" ("part_id","sort_order");--> statement-breakpoint
CREATE UNIQUE INDEX "parts_key_key" ON "math"."parts" ("key");--> statement-breakpoint
CREATE UNIQUE INDEX "sections_chapter_key_key" ON "math"."sections" ("chapter_id","key");--> statement-breakpoint
ALTER TABLE "math"."activity" ADD CONSTRAINT "activity_chapter_id_chapters_id_fkey" FOREIGN KEY ("chapter_id") REFERENCES "math"."chapters"("id") ON DELETE CASCADE ON UPDATE CASCADE;--> statement-breakpoint
ALTER TABLE "math"."activity" ADD CONSTRAINT "activity_section_id_sections_id_fkey" FOREIGN KEY ("section_id") REFERENCES "math"."sections"("id") ON DELETE SET NULL ON UPDATE CASCADE;--> statement-breakpoint
ALTER TABLE "math"."chapters" ADD CONSTRAINT "chapters_part_id_parts_id_fkey" FOREIGN KEY ("part_id") REFERENCES "math"."parts"("id") ON DELETE CASCADE ON UPDATE CASCADE;--> statement-breakpoint
ALTER TABLE "math"."sections" ADD CONSTRAINT "sections_chapter_id_chapters_id_fkey" FOREIGN KEY ("chapter_id") REFERENCES "math"."chapters"("id") ON DELETE CASCADE ON UPDATE CASCADE;--> statement-breakpoint
-- Seed: the Napkin outline (parts, chapters with PDF page ranges, exercise counts and approximate
-- prereqs, sections) from Terence Tao's extraction of the book (2026-10-26). Progress starts empty.
INSERT INTO "math"."parts" ("key", "num", "title", "sort_order") VALUES
('1', 1, 'Starting Out', 1),
('2', 2, 'Basic Abstract Algebra', 2),
('3', 3, 'Basic Topology', 3),
('4', 4, 'Linear Algebra', 4),
('5', 5, 'More on Groups', 5),
('6', 6, 'Representation Theory', 6),
('7', 7, 'Quantum Algorithms', 7),
('8', 8, 'Calculus 101', 8),
('9', 9, 'Complex Analysis', 9),
('10', 10, 'Measure Theory', 10),
('11', 11, 'Probability (TO DO)', 11),
('12', 12, 'Differential Geometry', 12),
('13', 13, 'Riemann Surfaces', 13),
('14', 14, 'Algebraic NT I: Rings of Integers', 14),
('15', 15, 'Algebraic NT II: Galois and Ramification Theory', 15),
('16', 16, 'Algebraic Topology I: Homotopy', 16),
('17', 17, 'Category Theory', 17),
('18', 18, 'Algebraic Topology II: Homology', 18),
('19', 19, 'Algebraic Geometry I: Classical Varieties', 19),
('20', 20, 'Algebraic Geometry II: Affine Schemes', 20),
('21', 21, 'Set Theory I: ZFC, Ordinals, and Cardinals', 21),
('22', 22, 'Set Theory II: Model Theory and Forcing', 22),
('appendix', NULL, 'Appendix', 23);
--> statement-breakpoint
INSERT INTO "math"."chapters" ("key", "part_id", "title", "page_start", "page_end", "exercise_count", "prereqs", "sort_order") VALUES
('0', (SELECT id FROM "math"."parts" WHERE key = '1'), 'Sales pitches', 37, 42, NULL, '{}'::text[], 1),
('1', (SELECT id FROM "math"."parts" WHERE key = '1'), 'Groups', 43, 54, 9, '{}'::text[], 2),
('2', (SELECT id FROM "math"."parts" WHERE key = '1'), 'Metric spaces', 55, 68, 6, '{}'::text[], 3),
('3', (SELECT id FROM "math"."parts" WHERE key = '2'), 'Homomorphisms and quotient groups', 69, 80, 8, '{}'::text[], 4),
('4', (SELECT id FROM "math"."parts" WHERE key = '2'), 'Rings and ideals', 81, 92, 8, '{}'::text[], 5),
('5', (SELECT id FROM "math"."parts" WHERE key = '2'), 'Flavors of rings', 93, 108, 9, '{}'::text[], 6),
('6', (SELECT id FROM "math"."parts" WHERE key = '3'), 'Properties of metric spaces', 109, 114, 5, '{}'::text[], 7),
('7', (SELECT id FROM "math"."parts" WHERE key = '3'), 'Topological spaces', 115, 124, 8, '{}'::text[], 8),
('8', (SELECT id FROM "math"."parts" WHERE key = '3'), 'Compactness', 125, 138, 9, '{}'::text[], 9),
('9', (SELECT id FROM "math"."parts" WHERE key = '4'), 'Vector spaces', 139, 156, 9, '{1,3,4,5}'::text[], 10),
('10', (SELECT id FROM "math"."parts" WHERE key = '4'), 'Eigen-things', 157, 166, 7, '{1,3,4,5}'::text[], 11),
('11', (SELECT id FROM "math"."parts" WHERE key = '4'), 'Dual space and trace', 167, 174, 4, '{1,3,4,5}'::text[], 12),
('12', (SELECT id FROM "math"."parts" WHERE key = '4'), 'Determinant', 175, 182, 8, '{1,3,4,5}'::text[], 13),
('13', (SELECT id FROM "math"."parts" WHERE key = '4'), 'Inner product spaces', 183, 190, 8, '{1,3,4,5}'::text[], 14),
('14', (SELECT id FROM "math"."parts" WHERE key = '4'), 'Bonus: Fourier analysis', 191, 200, 2, '{1,3,4,5}'::text[], 15),
('15', (SELECT id FROM "math"."parts" WHERE key = '4'), 'Duals, adjoint, and transposes', 201, 212, 8, '{1,3,4,5}'::text[], 16),
('16', (SELECT id FROM "math"."parts" WHERE key = '5'), 'Group actions overkill AIME problems', 213, 218, 5, '{1,3,4,5}'::text[], 17),
('17', (SELECT id FROM "math"."parts" WHERE key = '5'), 'Find all groups', 219, 224, 5, '{16}'::text[], 18),
('18', (SELECT id FROM "math"."parts" WHERE key = '5'), 'The PID structure theorem', 225, 236, 5, '{1,3,4,5}'::text[], 19),
('19', (SELECT id FROM "math"."parts" WHERE key = '6'), 'Representations of algebras', 237, 248, 5, '{9,10,11,12,13,14,15,18}'::text[], 20),
('20', (SELECT id FROM "math"."parts" WHERE key = '6'), 'Semisimple algebras', 249, 256, 5, '{9,10,11,12,13,14,15,18}'::text[], 21),
('21', (SELECT id FROM "math"."parts" WHERE key = '6'), 'Characters', 257, 264, 5, '{9,10,11,12,13,14,15,18}'::text[], 22),
('22', (SELECT id FROM "math"."parts" WHERE key = '6'), 'Some applications', 265, 270, NULL, '{9,10,11,12,13,14,15,18}'::text[], 23),
('23', (SELECT id FROM "math"."parts" WHERE key = '7'), 'Quantum states and measurements', 271, 278, 2, '{9,10,11,12,13,14,15,18}'::text[], 24),
('24', (SELECT id FROM "math"."parts" WHERE key = '7'), 'Quantum circuits', 279, 286, 4, '{9,10,11,12,13,14,15,18}'::text[], 25),
('25', (SELECT id FROM "math"."parts" WHERE key = '7'), 'Shor''s algorithm', 287, 294, NULL, '{9,10,11,12,13,14,15,18}'::text[], 26),
('26', (SELECT id FROM "math"."parts" WHERE key = '8'), 'Limits and series', 295, 306, 8, '{2,6,7,8}'::text[], 27),
('27', (SELECT id FROM "math"."parts" WHERE key = '8'), 'Bonus: A hint of p-adic numbers', 307, 318, 4, '{2,6,7,8}'::text[], 28),
('28', (SELECT id FROM "math"."parts" WHERE key = '8'), 'Differentiation', 319, 330, 7, '{2,6,7,8}'::text[], 29),
('29', (SELECT id FROM "math"."parts" WHERE key = '8'), 'Power series and Taylor series', 331, 338, 5, '{2,6,7,8}'::text[], 30),
('30', (SELECT id FROM "math"."parts" WHERE key = '8'), 'Riemann integrals', 339, 348, 4, '{2,6,7,8}'::text[], 31),
('31', (SELECT id FROM "math"."parts" WHERE key = '9'), 'Holomorphic functions', 349, 364, 6, '{26,27,28,29,30}'::text[], 32),
('32', (SELECT id FROM "math"."parts" WHERE key = '9'), 'Meromorphic functions', 365, 374, 5, '{26,27,28,29,30}'::text[], 33),
('33', (SELECT id FROM "math"."parts" WHERE key = '9'), 'Holomorphic square roots and logarithms', 375, 380, 2, '{26,27,28,29,30}'::text[], 34),
('34', (SELECT id FROM "math"."parts" WHERE key = '9'), 'Bonus: Topological Abel-Ruffini Theorem', 381, 386, 1, '{26,27,28,29,30}'::text[], 35),
('35', (SELECT id FROM "math"."parts" WHERE key = '10'), 'Measure spaces', 387, 394, 2, '{26,27,28,29,30}'::text[], 36),
('36', (SELECT id FROM "math"."parts" WHERE key = '10'), 'Constructing the Borel and Lebesgue measure', 395, 404, 3, '{26,27,28,29,30}'::text[], 37),
('37', (SELECT id FROM "math"."parts" WHERE key = '10'), 'Lebesgue integration', 405, 410, 2, '{26,27,28,29,30}'::text[], 38),
('38', (SELECT id FROM "math"."parts" WHERE key = '10'), 'Swapping order with Lebesgue integrals', 411, 418, NULL, '{26,27,28,29,30}'::text[], 39),
('39', (SELECT id FROM "math"."parts" WHERE key = '10'), 'Bonus: A hint of Pontryagin duality', 419, 426, 2, '{26,27,28,29,30}'::text[], 40),
('40', (SELECT id FROM "math"."parts" WHERE key = '11'), 'Random variables (TO DO)', 427, 428, 2, '{35,36,37,38,39}'::text[], 41),
('41', (SELECT id FROM "math"."parts" WHERE key = '11'), 'Large number laws (TO DO)', 429, 436, 2, '{35,36,37,38,39}'::text[], 42),
('42', (SELECT id FROM "math"."parts" WHERE key = '11'), 'Stopped martingales (TO DO)', 437, 452, 6, '{35,36,37,38,39}'::text[], 43),
('43', (SELECT id FROM "math"."parts" WHERE key = '12'), 'Multivariable calculus done correctly', 453, 460, 2, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 44),
('44', (SELECT id FROM "math"."parts" WHERE key = '12'), 'Differential forms', 461, 472, 2, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 45),
('45', (SELECT id FROM "math"."parts" WHERE key = '12'), 'Integrating differential forms', 473, 484, 7, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 46),
('46', (SELECT id FROM "math"."parts" WHERE key = '12'), 'A bit of manifolds', 485, 496, 1, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 47),
('47', (SELECT id FROM "math"."parts" WHERE key = '13'), 'Basic definitions of Riemann surfaces', 497, 504, NULL, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 48),
('48', (SELECT id FROM "math"."parts" WHERE key = '13'), 'Morphisms between Riemann surfaces', 505, 510, NULL, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 49),
('49', (SELECT id FROM "math"."parts" WHERE key = '13'), 'Affine and projective plane curves', 511, 520, NULL, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 50),
('50', (SELECT id FROM "math"."parts" WHERE key = '13'), 'Differential forms', 521, 524, NULL, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 51),
('51', (SELECT id FROM "math"."parts" WHERE key = '13'), 'The Riemann-Roch theorem', 525, 530, NULL, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 52),
('52', (SELECT id FROM "math"."parts" WHERE key = '13'), 'Line bundles', 531, 540, NULL, '{9,10,11,12,13,14,15,18,26,27,28,29,30}'::text[], 53),
('53', (SELECT id FROM "math"."parts" WHERE key = '14'), 'Algebraic integers', 541, 546, 9, '{9,10,11,12,13,14,15,18}'::text[], 54),
('54', (SELECT id FROM "math"."parts" WHERE key = '14'), 'The ring of integers', 547, 556, 6, '{9,10,11,12,13,14,15,18}'::text[], 55),
('55', (SELECT id FROM "math"."parts" WHERE key = '14'), 'Unique factorization (finally!)', 557, 568, 5, '{9,10,11,12,13,14,15,18}'::text[], 56),
('56', (SELECT id FROM "math"."parts" WHERE key = '14'), 'Minkowski bound and class groups', 569, 588, 6, '{9,10,11,12,13,14,15,18}'::text[], 57),
('57', (SELECT id FROM "math"."parts" WHERE key = '14'), 'More properties of the discriminant', 589, 590, 6, '{9,10,11,12,13,14,15,18}'::text[], 58),
('58', (SELECT id FROM "math"."parts" WHERE key = '14'), 'Bonus: Let''s solve Pell''s equation!', 591, 598, 2, '{9,10,11,12,13,14,15,18}'::text[], 59),
('59', (SELECT id FROM "math"."parts" WHERE key = '15'), 'Things Galois', 599, 610, 6, '{9,10,11,12,13,14,15,18}'::text[], 60),
('60', (SELECT id FROM "math"."parts" WHERE key = '15'), 'Finite fields', 611, 616, 1, '{53,54,55,56,57,58,59}'::text[], 61),
('61', (SELECT id FROM "math"."parts" WHERE key = '15'), 'Ramification theory', 617, 626, 1, '{53,54,55,56,57,58,59}'::text[], 62),
('62', (SELECT id FROM "math"."parts" WHERE key = '15'), 'The Frobenius element', 627, 638, 5, '{53,54,55,56,57,58,59}'::text[], 63),
('63', (SELECT id FROM "math"."parts" WHERE key = '15'), 'Bonus: A Bit on Artin Reciprocity', 639, 654, 3, '{53,54,55,56,57,58,59}'::text[], 64),
('64', (SELECT id FROM "math"."parts" WHERE key = '16'), 'Some topological constructions', 655, 666, 3, '{1,2,3,4,5,6,7,8}'::text[], 65),
('65', (SELECT id FROM "math"."parts" WHERE key = '16'), 'Fundamental groups', 667, 678, 4, '{1,2,3,4,5,6,7,8}'::text[], 66),
('66', (SELECT id FROM "math"."parts" WHERE key = '16'), 'Covering projections', 679, 692, NULL, '{1,2,3,4,5,6,7,8}'::text[], 67),
('67', (SELECT id FROM "math"."parts" WHERE key = '17'), 'Objects and morphisms', 693, 704, 4, '{1,3,4,5}'::text[], 68),
('68', (SELECT id FROM "math"."parts" WHERE key = '17'), 'Functors and natural transformations', 705, 716, 3, '{1,3,4,5}'::text[], 69),
('69', (SELECT id FROM "math"."parts" WHERE key = '17'), 'Limits in categories (TO DO)', 717, 718, 1, '{1,3,4,5}'::text[], 70),
('70', (SELECT id FROM "math"."parts" WHERE key = '17'), 'Abelian categories', 719, 728, 4, '{1,3,4,5}'::text[], 71),
('71', (SELECT id FROM "math"."parts" WHERE key = '18'), 'Singular homology', 729, 742, 2, '{64,65,66,67,68,69,70}'::text[], 72),
('72', (SELECT id FROM "math"."parts" WHERE key = '18'), 'The long exact sequence', 743, 754, 6, '{64,65,66,67,68,69,70}'::text[], 73),
('73', (SELECT id FROM "math"."parts" WHERE key = '18'), 'Excision and relative homology', 755, 762, 5, '{64,65,66,67,68,69,70}'::text[], 74),
('74', (SELECT id FROM "math"."parts" WHERE key = '18'), 'Bonus: Cellular homology', 763, 774, 5, '{64,65,66,67,68,69,70}'::text[], 75),
('75', (SELECT id FROM "math"."parts" WHERE key = '18'), 'Singular cohomology', 775, 786, 3, '{64,65,66,67,68,69,70}'::text[], 76),
('76', (SELECT id FROM "math"."parts" WHERE key = '18'), 'Application of cohomology', 787, 806, 4, '{64,65,66,67,68,69,70}'::text[], 77),
('77', (SELECT id FROM "math"."parts" WHERE key = '19'), 'Affine varieties', 807, 816, 3, '{1,3,4,5}'::text[], 78),
('78', (SELECT id FROM "math"."parts" WHERE key = '19'), 'Affine varieties as ringed spaces', 817, 826, 3, '{1,3,4,5}'::text[], 79),
('79', (SELECT id FROM "math"."parts" WHERE key = '19'), 'Projective varieties', 827, 834, NULL, '{1,3,4,5}'::text[], 80),
('80', (SELECT id FROM "math"."parts" WHERE key = '19'), 'Bonus: Bézout''s theorem', 835, 842, 2, '{1,3,4,5}'::text[], 81),
('81', (SELECT id FROM "math"."parts" WHERE key = '19'), 'Morphisms of varieties', 843, 854, 4, '{1,3,4,5}'::text[], 82),
('82', (SELECT id FROM "math"."parts" WHERE key = '20'), 'Sheaves and ringed spaces', 855, 866, 4, '{67,68,69,70,77,78,79,80,81}'::text[], 83),
('83', (SELECT id FROM "math"."parts" WHERE key = '20'), 'Localization', 867, 876, 4, '{67,68,69,70,77,78,79,80,81}'::text[], 84),
('84', (SELECT id FROM "math"."parts" WHERE key = '20'), 'Affine schemes: the Zariski topology', 877, 886, 3, '{67,68,69,70,77,78,79,80,81}'::text[], 85),
('85', (SELECT id FROM "math"."parts" WHERE key = '20'), 'Affine schemes: the sheaf', 887, 896, 5, '{67,68,69,70,77,78,79,80,81}'::text[], 86),
('86', (SELECT id FROM "math"."parts" WHERE key = '20'), 'Interlude: eighteen examples of affine schemes', 897, 912, 4, '{67,68,69,70,77,78,79,80,81}'::text[], 87),
('87', (SELECT id FROM "math"."parts" WHERE key = '20'), 'Morphisms of locally ringed spaces', 913, 926, 2, '{67,68,69,70,77,78,79,80,81}'::text[], 88),
('88', (SELECT id FROM "math"."parts" WHERE key = '21'), 'Interlude: Cauchy''s functional equation and Zorn''s lemma', 927, 934, 3, '{}'::text[], 89),
('89', (SELECT id FROM "math"."parts" WHERE key = '21'), 'Zermelo-Fraenkel with choice', 935, 942, 5, '{}'::text[], 90),
('90', (SELECT id FROM "math"."parts" WHERE key = '21'), 'Ordinals', 943, 954, 3, '{}'::text[], 91),
('91', (SELECT id FROM "math"."parts" WHERE key = '21'), 'Cardinals', 955, 964, 4, '{}'::text[], 92),
('92', (SELECT id FROM "math"."parts" WHERE key = '22'), 'Inner model theory', 965, 976, 4, '{}'::text[], 93),
('93', (SELECT id FROM "math"."parts" WHERE key = '22'), 'Forcing', 977, 986, 2, '{}'::text[], 94),
('94', (SELECT id FROM "math"."parts" WHERE key = '22'), 'Breaking the continuum hypothesis', 987, 994, 1, '{}'::text[], 95),
('A', (SELECT id FROM "math"."parts" WHERE key = 'appendix'), 'Pedagogical comments and references', 995, 998, NULL, '{}'::text[], 96),
('B', (SELECT id FROM "math"."parts" WHERE key = 'appendix'), 'Hints to selected problems', 999, 1010, NULL, '{}'::text[], 97),
('C', (SELECT id FROM "math"."parts" WHERE key = 'appendix'), 'Sketches of selected solutions', 1011, 1038, NULL, '{}'::text[], 98),
('D', (SELECT id FROM "math"."parts" WHERE key = 'appendix'), 'Glossary of notations', 1039, 1048, NULL, '{}'::text[], 99),
('E', (SELECT id FROM "math"."parts" WHERE key = 'appendix'), 'Terminology on sets and functions', 1049, 1052, NULL, '{}'::text[], 100);
--> statement-breakpoint
INSERT INTO "math"."sections" ("chapter_id", "key", "title", "sort_order")
SELECT c.id, v.key, v.title, v.ord FROM (VALUES
('0', '0.1', 'The basics', 1),
('0', '0.2', 'Abstract algebra', 2),
('0', '0.3', 'Real and complex analysis', 3),
('0', '0.4', 'Algebraic number theory', 4),
('0', '0.5', 'Algebraic topology', 5),
('0', '0.6', 'Algebraic geometry', 6),
('0', '0.7', 'Set theory', 7),
('1', '1.1', 'Definition and examples of groups', 1),
('1', '1.2', 'Properties of groups', 2),
('1', '1.3', 'Isomorphisms', 3),
('1', '1.4', 'Orders of groups, and Lagrange''s theorem', 4),
('1', '1.5', 'Subgroups', 5),
('1', '1.6', 'Groups of small orders', 6),
('1', '1.7', 'Unimportant long digression', 7),
('1', '1.8', 'A few harder problems to think about', 8),
('2', '2.1', 'Definition and examples of metric spaces', 1),
('2', '2.2', 'Convergence in metric spaces', 2),
('2', '2.3', 'Continuous maps', 3),
('2', '2.4', 'Homeomorphisms', 4),
('2', '2.5', 'Extended example/definition: product metric', 5),
('2', '2.6', 'Open sets', 6),
('2', '2.7', 'Closed sets', 7),
('2', '2.8', 'A few harder problems to think about', 8),
('3', '3.1', 'Generators and group presentations', 1),
('3', '3.2', 'Homomorphisms', 2),
('3', '3.3', 'Cosets and modding out', 3),
('3', '3.4', '(Optional) Proof of Lagrange''s theorem', 4),
('3', '3.5', 'Eliminating the homomorphism', 5),
('3', '3.6', '(Digression) The first isomorphism theorem', 6),
('3', '3.7', 'A few harder problems to think about', 7),
('4', '4.1', 'Some motivational metaphors about rings vs groups', 1),
('4', '4.2', '(Optional) Pedagogical notes on motivation', 2),
('4', '4.3', 'Definition and examples of rings', 3),
('4', '4.4', 'Fields', 4),
('4', '4.5', 'Homomorphisms', 5),
('4', '4.6', 'Ideals', 6),
('4', '4.7', 'Generating ideals', 7),
('4', '4.8', 'Principal ideal domains', 8),
('4', '4.9', 'Noetherian rings', 9),
('4', '4.10', 'A few harder problems to think about', 10),
('5', '5.1', 'Fields', 1),
('5', '5.2', 'Integral domains', 2),
('5', '5.3', 'Prime ideals', 3),
('5', '5.4', 'Maximal ideals', 4),
('5', '5.5', 'Field of fractions', 5),
('5', '5.6', 'Unique factorization domains (UFD''s)', 6),
('5', '5.7', 'Extra: Euclidean domains', 7),
('5', '5.8', 'A few harder problems to think about', 8),
('6', '6.1', 'Boundedness', 1),
('6', '6.2', 'Completeness', 2),
('6', '6.3', 'Let the buyer beware', 3),
('6', '6.4', 'Subspaces, and (inb4) a confusing linguistic point', 4),
('6', '6.5', 'A few harder problems to think about', 5),
('7', '7.1', 'Forgetting the metric', 1),
('7', '7.2', 'Re-definitions', 2),
('7', '7.3', 'Hausdorff spaces', 3),
('7', '7.4', 'Subspaces', 4),
('7', '7.5', 'Connected spaces', 5),
('7', '7.6', 'Path-connected spaces', 6),
('7', '7.7', 'Homotopy and simply connected spaces', 7),
('7', '7.8', 'Bases of spaces', 8),
('7', '7.9', 'A few harder problems to think about', 9),
('8', '8.1', 'Definition of sequential compactness', 1),
('8', '8.2', 'Criteria for compactness', 2),
('8', '8.3', 'Compactness using open covers', 3),
('8', '8.4', 'Applications of compactness', 4),
('8', '8.5', '(Optional) Equivalence of formulations of compactness', 5),
('8', '8.6', 'A few harder problems to think about', 6),
('9', '9.1', 'The definitions of a ring and field', 1),
('9', '9.2', 'Modules and vector spaces', 2),
('9', '9.3', 'Direct sums', 3),
('9', '9.4', 'Linear independence, spans, and basis', 4),
('9', '9.5', 'Linear maps', 5),
('9', '9.6', 'What is a matrix?', 6),
('9', '9.7', 'Subspaces and picking convenient bases', 7),
('9', '9.8', 'A cute application: Lagrange interpolation', 8),
('9', '9.9', 'Pedagogical digression: Arrays of numbers are evil', 9),
('9', '9.10', 'A word on general modules', 10),
('9', '9.11', 'A few harder problems to think about', 11),
('10', '10.1', 'Why you should care', 1),
('10', '10.2', 'Warning on assumptions', 2),
('10', '10.3', 'Eigenvectors and eigenvalues', 3),
('10', '10.4', 'The Jordan form', 4),
('10', '10.5', 'Nilpotent maps', 5),
('10', '10.6', 'Reducing to the nilpotent case', 6),
('10', '10.7', '(Optional) Proof of nilpotent Jordan', 7),
('10', '10.8', 'Algebraic and geometric multiplicity', 8),
('10', '10.9', 'A few harder problems to think about', 9),
('11', '11.1', 'Tensor product', 1),
('11', '11.2', 'Dual space', 2),
('11', '11.3', 'VW gives matrices from V to W', 3),
('11', '11.4', 'The trace', 4),
('11', '11.5', 'A few harder problems to think about', 5),
('12', '12.1', 'Wedge product', 1),
('12', '12.2', 'The determinant', 2),
('12', '12.3', 'Characteristic polynomials, and Cayley-Hamilton', 3),
('12', '12.4', 'A few harder problems to think about', 4),
('13', '13.1', 'The inner product', 1),
('13', '13.2', 'Norms', 2),
('13', '13.3', 'Orthogonality', 3),
('13', '13.4', 'Hilbert spaces', 4),
('13', '13.5', 'A few harder problems to think about', 5),
('14', '14.1', 'Synopsis', 1),
('14', '14.2', 'A reminder on Hilbert spaces', 2),
('14', '14.3', 'Common examples', 3),
('14', '14.4', 'Summary, and another teaser', 4),
('14', '14.5', 'Parseval and friends', 5),
('14', '14.6', 'Application: Basel problem', 6),
('14', '14.7', 'Application: Arrow''s Impossibility Theorem', 7),
('14', '14.8', 'A few harder problems to think about', 8),
('15', '15.1', 'Dual of a map', 1),
('15', '15.2', '(Optional) A cautionary tale on what goes wrong if you don''t have a natural isomorphism V .5-.5.5-.5.5-.5.5-.5V', 2),
('15', '15.3', 'Identifying with the dual space', 3),
('15', '15.4', 'The adjoint (conjugate transpose)', 4),
('15', '15.5', 'Eigenvalues of normal maps', 5),
('15', '15.6', 'A few harder problems to think about', 6),
('16', '16.1', 'Definition of a group action', 1),
('16', '16.2', 'Stabilizers and orbits', 2),
('16', '16.3', 'Burnside''s lemma', 3),
('16', '16.4', 'Conjugation of elements', 4),
('16', '16.5', 'A few harder problems to think about', 5),
('17', '17.1', 'Sylow theorems', 1),
('17', '17.2', '(Optional) Proving Sylow''s theorem', 2),
('17', '17.3', '(Optional) Simple groups and Jordan-Hölder', 3),
('17', '17.4', 'A few harder problems to think about', 4),
('18', '18.1', 'Finitely generated abelian groups', 1),
('18', '18.2', 'Some ring theory prerequisites', 2),
('18', '18.3', 'The structure theorem', 3),
('18', '18.4', 'Reduction to maps of free R-modules', 4),
('18', '18.5', 'Uniqueness of primary form', 5),
('18', '18.6', 'Smith normal form', 6),
('18', '18.7', 'A few harder problems to think about', 7),
('19', '19.1', 'Algebras', 1),
('19', '19.2', 'Representations', 2),
('19', '19.3', 'Direct sums', 3),
('19', '19.4', 'Irreducible and indecomposable representations', 4),
('19', '19.5', 'Morphisms of representations', 5),
('19', '19.6', 'The representations of Matd(k)', 6),
('19', '19.7', 'A few harder problems to think about', 7),
('20', '20.1', 'Schur''s lemma continued', 1),
('20', '20.2', 'Density theorem', 2),
('20', '20.3', 'Semisimple algebras', 3),
('20', '20.4', 'Maschke''s theorem', 4),
('20', '20.5', 'Example: the representations of C[S3]', 5),
('20', '20.6', 'A few harder problems to think about', 6),
('21', '21.1', 'Definitions', 1),
('21', '21.2', 'The dual space modulo the commutator', 2),
('21', '21.3', 'Orthogonality of characters', 3),
('21', '21.4', 'Examples of character tables', 4),
('21', '21.5', 'A few harder problems to think about', 5),
('22', '22.1', 'Frobenius divisibility', 1),
('22', '22.2', 'Burnside''s theorem', 2),
('22', '22.3', 'Frobenius determinant', 3),
('23', '23.1', 'Bra-ket notation', 1),
('23', '23.2', 'The state space', 2),
('23', '23.3', 'Observations', 3),
('23', '23.4', 'Entanglement', 4),
('23', '23.5', 'A few harder problems to think about', 5),
('24', '24.1', 'Classical logic gates', 1),
('24', '24.2', 'Reversible classical logic', 2),
('24', '24.3', 'Quantum logic gates', 3),
('24', '24.4', 'Deutsch-Jozsa algorithm', 4),
('24', '24.5', 'A few harder problems to think about', 5),
('25', '25.1', 'The classical (inverse) Fourier transform', 1),
('25', '25.2', 'The quantum Fourier transform', 2),
('25', '25.3', 'Shor''s algorithm', 3),
('26', '26.1', 'Completeness and inf/sup', 1),
('26', '26.2', 'Proofs of the two key completeness properties of R', 2),
('26', '26.3', 'Monotonic sequences', 3),
('26', '26.4', 'Infinite series', 4),
('26', '26.5', 'Series addition is not commutative: a horror story', 5),
('26', '26.6', 'Limits of functions at points', 6),
('26', '26.7', 'Limits of functions at infinity', 7),
('26', '26.8', 'A few harder problems to think about', 8),
('27', '27.1', 'Motivation', 1),
('27', '27.2', 'Algebraic perspective', 2),
('27', '27.3', 'Analytic perspective', 3),
('27', '27.4', 'Mahler coefficients', 4),
('27', '27.5', 'A few harder problems to think about', 5),
('28', '28.1', 'Definition', 1),
('28', '28.2', 'How to compute them', 2),
('28', '28.3', 'Local (and global) maximums', 3),
('28', '28.4', 'Rolle and friends', 4),
('28', '28.5', 'Smooth functions', 5),
('28', '28.6', 'A few harder problems to think about', 6),
('29', '29.1', 'Motivation', 1),
('29', '29.2', 'Power series', 2),
('29', '29.3', 'Differentiating them', 3),
('29', '29.4', 'Analytic functions', 4),
('29', '29.5', 'A definition of Euler''s constant and exponentiation', 5),
('29', '29.6', 'This all works over complex numbers as well, except also complex analysis is heaven', 6),
('29', '29.7', 'A few harder problems to think about', 7),
('30', '30.1', 'Uniform continuity', 1),
('30', '30.2', 'Dense sets and extension', 2),
('30', '30.3', 'Defining the Riemann integral', 3),
('30', '30.4', 'Meshes', 4),
('30', '30.5', 'A few harder problems to think about', 5),
('31', '31.1', 'The nicest functions on earth', 1),
('31', '31.2', 'Complex differentiation', 2),
('31', '31.3', 'Contour integrals', 3),
('31', '31.4', 'Cauchy-Goursat theorem', 4),
('31', '31.5', 'Cauchy''s integral theorem', 5),
('31', '31.6', 'Holomorphic functions are analytic', 6),
('31', '31.7', 'Optional: Proof that holomorphic functions are analytic', 7),
('31', '31.8', 'A few harder problems to think about', 8),
('32', '32.1', 'The second nicest functions on earth', 1),
('32', '32.2', 'Meromorphic functions', 2),
('32', '32.3', 'Winding numbers and the residue theorem', 3),
('32', '32.4', 'Argument principle', 4),
('32', '32.5', 'Digression: the Argument Principle viewed geometrically', 5),
('32', '32.6', 'Philosophy: why are holomorphic functions so nice?', 6),
('32', '32.7', 'A few harder problems to think about', 7),
('33', '33.1', 'Motivation: square root of a complex number', 1),
('33', '33.2', 'Square roots of holomorphic functions', 2),
('33', '33.3', 'Covering projections', 3),
('33', '33.4', 'Complex logarithms', 4),
('33', '33.5', 'Some special cases', 5),
('33', '33.6', 'A few harder problems to think about', 6),
('34', '34.1', 'The Game Plan', 1),
('34', '34.2', 'Step 1: The Simplest Case', 2),
('34', '34.3', 'Step 2: Nested Roots', 3),
('34', '34.4', 'Step 3: Normal Groups', 4),
('34', '34.5', 'Summary', 5),
('34', '34.6', 'A few harder problems to think about', 6),
('35', '35.1', 'Letter connotations', 1),
('35', '35.2', 'Motivating measure spaces via random variables', 2),
('35', '35.3', 'Motivating measure spaces geometrically', 3),
('35', '35.4', 'σ-algebras and measurable spaces', 4),
('35', '35.5', 'Measure spaces', 5),
('35', '35.6', 'A hint of Banach-Tarski', 6),
('35', '35.7', 'Measurable functions', 7),
('35', '35.8', 'On the word ``almost''''', 8),
('35', '35.9', 'A few harder problems to think about', 9),
('36', '36.1', 'Pre-measures', 1),
('36', '36.2', 'Outer measures', 2),
('36', '36.3', 'Carathéodory extension for outer measures', 3),
('36', '36.4', 'Defining the Lebesgue measure', 4),
('36', '36.5', 'A fourth row: Carathéodory for pre-measures', 5),
('36', '36.6', 'From now on, we assume the Borel measure', 6),
('36', '36.7', 'A few harder problems to think about', 7),
('37', '37.1', 'The definition', 1),
('37', '37.2', 'An equivalent definition', 2),
('37', '37.3', 'Relation to Riemann integrals (or: actually computing Lebesgue integrals)', 3),
('37', '37.4', 'A few harder problems to think about', 4),
('38', '38.1', 'Motivating limit interchange', 1),
('38', '38.2', 'Overview', 2),
('38', '38.3', 'Fatou''s lemma', 3),
('38', '38.4', 'Everything else', 4),
('38', '38.5', 'Fubini and Tonelli', 5),
('38', '38.6', 'A few harder problems to think about', 6),
('39', '39.1', 'LCA groups', 1),
('39', '39.2', 'The Pontryagin dual', 2),
('39', '39.3', 'The orthonormal basis in the compact case', 3),
('39', '39.4', 'The Fourier transform of the non-compact case', 4),
('39', '39.5', 'Summary', 5),
('39', '39.6', 'A few harder problems to think about', 6),
('40', '40.1', 'Random variables', 1),
('40', '40.2', 'Distribution functions', 2),
('40', '40.3', 'Examples of random variables', 3),
('40', '40.4', 'Characteristic functions', 4),
('40', '40.5', 'Independent random variables', 5),
('40', '40.6', 'A few harder problems to think about', 6),
('41', '41.1', 'Notions of convergence', 1),
('41', '41.2', 'Weak law of large numbers', 2),
('41', '41.3', 'Strong law of large numbers', 3),
('41', '41.4', 'A few harder problems to think about', 4),
('42', '42.1', 'How to make money almost surely', 1),
('42', '42.2', 'Sub-σ-algebras and filtrations', 2),
('42', '42.3', 'Conditional expectation', 3),
('42', '42.4', 'Supermartingales', 4),
('42', '42.5', 'Optional stopping', 5),
('42', '42.6', 'Fun applications of optional stopping (TO DO)', 6),
('42', '42.7', 'A few harder problems to think about', 7),
('43', '43.1', 'The total derivative', 1),
('43', '43.2', 'The projection principle', 2),
('43', '43.3', 'Total and partial derivatives', 3),
('43', '43.4', '(Optional) A word on higher derivatives', 4),
('43', '43.5', 'Towards differential forms', 5),
('43', '43.6', 'A few harder problems to think about', 6),
('44', '44.1', 'Pictures of differential forms', 1),
('44', '44.2', 'Pictures of exterior derivatives', 2),
('44', '44.3', 'Differential forms', 3),
('44', '44.4', 'Exterior derivatives', 4),
('44', '44.5', 'Digression: k(V) versus (k(V))', 5),
('44', '44.6', 'Tangential remark: Arc length ds is not a 1-form', 6),
('44', '44.7', 'Closed and exact forms', 7),
('44', '44.8', 'A few harder problems to think about', 8),
('45', '45.1', 'Motivation: line integrals', 1),
('45', '45.2', 'Pullbacks', 2),
('45', '45.3', 'Cells', 3),
('45', '45.4', 'Boundaries', 4),
('45', '45.5', 'Stokes'' theorem', 5),
('45', '45.6', 'Back to Earth: A comparison to what you learned in vector calculus', 6),
('45', '45.7', 'A few harder problems to think about', 7),
('46', '46.1', 'Topological manifolds', 1),
('46', '46.2', 'Smooth manifolds', 2),
('46', '46.3', 'Regular value theorem', 3),
('46', '46.4', 'Differential forms on manifolds', 4),
('46', '46.5', 'Orientations', 5),
('46', '46.6', 'Stokes'' theorem for manifolds', 6),
('46', '46.7', '(Optional) The tangent and cotangent space', 7),
('46', '46.8', 'A few harder problems to think about', 8),
('47', '47.1', 'Complex structures', 1),
('47', '47.2', 'Riemann surface', 2),
('47', '47.3', 'Complex manifold', 3),
('47', '47.4', 'Examples of Riemann surfaces', 4),
('48', '48.1', 'Definition', 1),
('48', '48.2', 'Functions to the Riemann sphere', 2),
('48', '48.3', 'Some other nice properties', 3),
('48', '48.4', 'Multiplicity of a map', 4),
('48', '48.5', 'The sum of the orders of a meromorphic function', 5),
('48', '48.6', 'The Hurwitz formula', 6),
('48', '48.7', 'The identity theorem', 7),
('49', '49.1', 'Affine plane curves', 1),
('49', '49.2', 'The projective line CP1', 2),
('49', '49.3', 'Projective plane curves', 3),
('49', '49.4', 'Filling in the holes', 4),
('49', '49.5', 'Nodes of a plane curve', 5),
('50', '50.1', 'Differential form on C', 1),
('50', '50.2', 'Visualization of differential forms', 2),
('51', '51.1', 'Motivation', 1),
('51', '51.2', 'Divisors', 2),
('51', '51.3', 'Degree of a divisor', 3),
('51', '51.4', 'The principal divisor of a meromorphic function', 4),
('51', '51.5', 'The Riemann-Roch theorem', 5),
('52', '52.1', 'Overview', 1),
('52', '52.2', 'Definition', 2),
('52', '52.3', 'Visualizing a line bundle', 3),
('52', '52.4', 'Morphisms between line bundles', 4),
('52', '52.5', 'Relation to invertible sheaves', 5),
('53', '53.1', 'Motivation from high school algebra', 1),
('53', '53.2', 'Algebraic numbers and algebraic integers', 2),
('53', '53.3', 'Number fields', 3),
('53', '53.4', 'Primitive element theorem, and monogenic extensions', 4),
('53', '53.5', 'A few harder problems to think about', 5),
('54', '54.1', 'Norms and traces', 1),
('54', '54.2', 'The ring of integers', 2),
('54', '54.3', 'On monogenic extensions', 3),
('54', '54.4', 'A few harder problems to think about', 4),
('55', '55.1', 'Motivation', 1),
('55', '55.2', 'Ideal arithmetic', 2),
('55', '55.3', 'Dedekind domains', 3),
('55', '55.4', 'Unique factorization works', 4),
('55', '55.5', 'The factoring algorithm', 5),
('55', '55.6', 'Fractional ideals', 6),
('55', '55.7', 'The ideal norm', 7),
('55', '55.8', 'A few harder problems to think about', 8),
('56', '56.1', 'The class group', 1),
('56', '56.2', 'The discriminant of a number field', 2),
('56', '56.3', 'The signature of a number field', 3),
('56', '56.4', 'Minkowski''s theorem', 4),
('56', '56.5', 'The trap box', 5),
('56', '56.6', 'The Minkowski bound', 6),
('56', '56.7', 'The class group is finite', 7),
('56', '56.8', 'Computation of class numbers', 8),
('56', '56.9', 'Optional: Proof that OK is a free Z-module', 9),
('56', '56.10', 'A few harder problems to think about', 10),
('57', '57.1', 'A few harder problems to think about', 1),
('58', '58.1', 'Units', 1),
('58', '58.2', 'Dirichlet''s unit theorem', 2),
('58', '58.3', 'Finding fundamental units', 3),
('58', '58.4', 'Pell''s equation', 4),
('58', '58.5', 'A few harder problems to think about', 5),
('59', '59.1', 'Motivation', 1),
('59', '59.2', 'Field extensions, algebraic extension, and splitting fields', 2),
('59', '59.3', 'Embeddings into algebraic closures for number fields', 3),
('59', '59.4', 'Everyone hates characteristic 2: separable vs irreducible', 4),
('59', '59.5', 'Automorphism groups and Galois extensions', 5),
('59', '59.6', 'Fundamental theorem of Galois theory', 6),
('59', '59.7', 'A few harder problems to think about', 7),
('59', '59.8', '(Optional) Proof that Galois extensions are splitting', 8),
('60', '60.1', 'Example of a finite field', 1),
('60', '60.2', 'Finite fields have prime power order', 2),
('60', '60.3', 'All finite fields are isomorphic', 3),
('60', '60.4', 'The Galois theory of finite fields', 4),
('60', '60.5', 'Extra: The multiplicative group of a finite field', 5),
('60', '60.6', 'A few harder problems to think about', 6),
('61', '61.1', 'Ramified / inert / split primes', 1),
('61', '61.2', 'Primes ramify if and only if they divide K', 2),
('61', '61.3', 'Inertial degrees', 3),
('61', '61.4', 'The magic of Galois extensions', 4),
('61', '61.5', '(Optional) Decomposition and inertia groups', 5),
('61', '61.6', 'Tangential remark: more general Galois extensions', 6),
('61', '61.7', 'A few harder problems to think about', 7),
('62', '62.1', 'Frobenius elements', 1),
('62', '62.2', 'Conjugacy classes', 2),
('62', '62.3', 'Chebotarev density theorem', 3),
('62', '62.4', 'Example: Frobenius elements of cyclotomic fields', 4),
('62', '62.5', 'Frobenius elements behave well with restriction', 5),
('62', '62.6', 'Application: Quadratic reciprocity', 6),
('62', '62.7', 'Frobenius elements control factorization', 7),
('62', '62.8', 'Example application: IMO 2003 problem 6', 8),
('62', '62.9', 'A few harder problems to think about', 9),
('63', '63.1', 'Overview', 1),
('63', '63.2', 'Infinite primes', 2),
('63', '63.3', 'Modular arithmetic with infinite primes', 3),
('63', '63.4', 'Infinite primes in extensions', 4),
('63', '63.5', 'Frobenius element and Artin symbol', 5),
('63', '63.6', 'Artin reciprocity', 6),
('63', '63.7', 'Application: Generalization of sum of two squares', 7),
('63', '63.8', 'A few harder problems to think about', 8),
('64', '64.1', 'Spheres', 1),
('64', '64.2', 'Quotient topology', 2),
('64', '64.3', 'Product topology', 3),
('64', '64.4', 'Disjoint union and wedge sum', 4),
('64', '64.5', 'CW complexes', 5),
('64', '64.6', 'The torus, Klein bottle, RPn, CPn', 6),
('64', '64.7', 'A few harder problems to think about', 7),
('65', '65.1', 'Fusing paths together', 1),
('65', '65.2', 'Fundamental groups', 2),
('65', '65.3', 'Fundamental groups are invariant under homeomorphism', 3),
('65', '65.4', 'Higher homotopy groups', 4),
('65', '65.5', 'Homotopy equivalent spaces', 5),
('65', '65.6', 'The pointed homotopy category', 6),
('65', '65.7', 'A few harder problems to think about', 7),
('66', '66.1', 'Even coverings and covering projections', 1),
('66', '66.2', 'Lifting theorem', 2),
('66', '66.3', 'Lifting correspondence', 3),
('66', '66.4', 'Regular coverings', 4),
('66', '66.5', 'The algebra of fundamental groups', 5),
('66', '66.6', 'A few harder problems to think about', 6),
('67', '67.1', 'Motivation: isomorphisms', 1),
('67', '67.2', 'Categories, and examples thereof', 2),
('67', '67.3', 'Special objects in categories', 3),
('67', '67.4', 'Binary products', 4),
('67', '67.5', 'Monic and epic maps', 5),
('67', '67.6', 'A few harder problems to think about', 6),
('68', '68.1', 'Many examples of functors', 1),
('68', '68.2', 'Covariant functors', 2),
('68', '68.3', 'Covariant functors as indexed family of objects', 3),
('68', '68.4', 'Contravariant functors', 4),
('68', '68.5', 'Equivalence of categories', 5),
('68', '68.6', '(Optional) Natural transformations', 6),
('68', '68.7', '(Optional) The Yoneda lemma', 7),
('68', '68.8', 'A few harder problems to think about', 8),
('69', '69.1', 'Equalizers', 1),
('69', '69.2', 'Pullback squares (TO DO)', 2),
('69', '69.3', 'Limits', 3),
('69', '69.4', 'A few harder problems to think about', 4),
('70', '70.1', 'Zero objects, kernels, cokernels, and images', 1),
('70', '70.2', 'Additive and abelian categories', 2),
('70', '70.3', 'Exact sequences', 3),
('70', '70.4', 'The Freyd-Mitchell embedding theorem', 4),
('70', '70.5', 'Breaking long exact sequences', 5),
('70', '70.6', 'A few harder problems to think about', 6),
('71', '71.1', 'Simplices and boundaries', 1),
('71', '71.2', 'The singular homology groups', 2),
('71', '71.3', 'The homology functor and chain complexes', 3),
('71', '71.4', 'More examples of chain complexes', 4),
('71', '71.5', 'A few harder problems to think about', 5),
('72', '72.1', 'Short exact sequences and four examples', 1),
('72', '72.2', 'The long exact sequence of homology groups', 2),
('72', '72.3', 'The Mayer-Vietoris sequence', 3),
('72', '72.4', 'A few harder problems to think about', 4),
('73', '73.1', 'Motivation', 1),
('73', '73.2', 'The long exact sequences', 2),
('73', '73.3', 'The category of pairs', 3),
('73', '73.4', 'Excision', 4),
('73', '73.5', 'Some applications', 5),
('73', '73.6', 'Invariance of dimension', 6),
('73', '73.7', 'A few harder problems to think about', 7),
('74', '74.1', 'Degrees', 1),
('74', '74.2', 'Cellular chain complex', 2),
('74', '74.3', 'Digression: why are the homology groups equal?', 3),
('74', '74.4', 'Application: Euler characteristic via Betti numbers', 4),
('74', '74.5', 'The cellular boundary formula', 5),
('74', '74.6', 'A few harder problems to think about', 6),
('75', '75.1', 'Cochain complexes', 1),
('75', '75.2', 'Cohomology of spaces', 2),
('75', '75.3', 'Cohomology of spaces is functorial', 3),
('75', '75.4', 'Universal coefficient theorem', 4),
('75', '75.5', 'Explanation for universal coefficient theorem', 5),
('75', '75.6', 'Example computation of cohomology groups', 6),
('75', '75.7', 'Visualization of cohomology groups', 7),
('75', '75.8', 'Relative cohomology groups', 8),
('75', '75.9', 'A few harder problems to think about', 9),
('76', '76.1', 'Poincaré duality', 1),
('76', '76.2', 'de Rham cohomology', 2),
('76', '76.3', 'Graded rings', 3),
('76', '76.4', 'Cup products', 4),
('76', '76.5', 'Relative cohomology pseudo-rings', 5),
('76', '76.6', 'Wedge sums', 6),
('76', '76.7', 'Cross product', 7),
('76', '76.8', 'Künneth formula', 8),
('76', '76.9', 'A few harder problems to think about', 9),
('77', '77.1', 'Affine varieties', 1),
('77', '77.2', 'Naming affine varieties via ideals', 2),
('77', '77.3', 'Radical ideals and Hilbert''s Nullstellensatz', 3),
('77', '77.4', 'Pictures of varieties in A1', 4),
('77', '77.5', 'Prime ideals correspond to irreducible affine varieties', 5),
('77', '77.6', 'Pictures in A2 and A3', 6),
('77', '77.7', 'Maximal ideals', 7),
('77', '77.8', 'Motivating schemes with non-radical ideals', 8),
('77', '77.9', 'A few harder problems to think about', 9),
('78', '78.1', 'Synopsis', 1),
('78', '78.2', 'The Zariski topology on An', 2),
('78', '78.3', 'The Zariski topology on affine varieties', 3),
('78', '78.4', 'Coordinate rings', 4),
('78', '78.5', 'The sheaf of regular functions', 5),
('78', '78.6', 'Regular functions on distinguished open sets', 6),
('78', '78.7', 'Baby ringed spaces', 7),
('78', '78.8', 'A few harder problems to think about', 8),
('79', '79.1', 'Graded rings', 1),
('79', '79.2', 'The ambient space', 2),
('79', '79.3', 'Homogeneous ideals', 3),
('79', '79.4', 'As ringed spaces', 4),
('79', '79.5', 'Examples of regular functions', 5),
('79', '79.6', 'A few harder problems to think about', 6),
('80', '80.1', 'Non-radical ideals', 1),
('80', '80.2', 'Hilbert functions of finitely many points', 2),
('80', '80.3', 'Hilbert polynomials', 3),
('80', '80.4', 'Bézout''s theorem', 4),
('80', '80.5', 'Applications', 5),
('80', '80.6', 'A few harder problems to think about', 6),
('81', '81.1', 'Defining morphisms of baby ringed spaces', 1),
('81', '81.2', 'Classifying the simplest examples', 2),
('81', '81.3', 'Some more applications and examples', 3),
('81', '81.4', 'The hyperbola effect', 4),
('81', '81.5', 'A few harder problems to think about', 5),
('82', '82.1', 'Motivation and warnings', 1),
('82', '82.2', 'Pre-sheaves', 2),
('82', '82.3', 'Stalks and germs', 3),
('82', '82.4', 'Sheaves', 4),
('82', '82.5', 'For sheaves, sections ``are'''' sequences of germs', 5),
('82', '82.6', 'Sheafification (optional)', 6),
('82', '82.7', 'A few harder problems to think about', 7),
('83', '83.1', 'Spoilers', 1),
('83', '83.2', 'The definition', 2),
('83', '83.3', 'Localization away from an element', 3),
('83', '83.4', 'Localization at a prime ideal', 4),
('83', '83.5', 'Prime ideals of localizations', 5),
('83', '83.6', 'Prime ideals of quotients', 6),
('83', '83.7', 'Localization commutes with quotients', 7),
('83', '83.8', 'A few harder problems to think about', 8),
('84', '84.1', 'Some more advertising', 1),
('84', '84.2', 'The set of points', 2),
('84', '84.3', 'The Zariski topology on the spectrum', 3),
('84', '84.4', 'Krull dimension', 4),
('84', '84.5', 'On radicals', 5),
('84', '84.6', 'A few harder problems to think about', 6),
('85', '85.1', 'A useless definition of the structure sheaf', 1),
('85', '85.2', 'The value of distinguished open sets (or: how to actually compute sections)', 2),
('85', '85.3', 'The stalks of the structure sheaf', 3),
('85', '85.4', 'Local rings and residue fields: linking germs to values', 4),
('85', '85.5', 'Recap', 5),
('85', '85.6', 'Functions are determined by germs, not values', 6),
('85', '85.7', 'A few harder problems to think about', 7),
('86', '86.1', 'Example: Spec k, a single point', 1),
('86', '86.2', 'Spec C[x], a one-dimensional line', 2),
('86', '86.3', 'Spec R[x], a one-dimensional line with complex conjugates glued (no fear nullstellensatz)', 3),
('86', '86.4', 'Spec k[x], over any ground field', 4),
('86', '86.5', 'Spec Z, a one-dimensional scheme', 5),
('86', '86.6', 'Spec k[x] / (x2-7x+12), two points', 6),
('86', '86.7', 'Spec k[x]/(x2), the double point', 7),
('86', '86.8', 'Spec k[x]/(x3-5x2), a double point and a single point', 8),
('86', '86.9', 'Spec Z/60Z, a scheme with three points', 9),
('86', '86.10', 'Spec k[x,y], the two-dimensional plane', 10),
('86', '86.11', 'Spec Z[x], a two-dimensional scheme, and Mumford''s picture', 11),
('86', '86.12', 'Spec k[x,y]/(y-x2), the parabola', 12),
('86', '86.13', 'Spec Z[i], the Gaussian integers (one-dimensional)', 13),
('86', '86.14', 'Long example: Spec k[x,y]/(xy), two axes', 14),
('86', '86.15', 'Spec k[x,x-1], the punctured line (or hyperbola)', 15),
('86', '86.16', 'Spec k[x](x), zooming in to the origin of the line', 16),
('86', '86.17', 'Spec k[x,y](x,y), zooming in to the origin of the plane', 17),
('86', '86.18', 'Spec k[x,y](0) = Spec k(x,y), the stalk above the generic point', 18),
('86', '86.19', 'A few harder problems to think about', 19),
('87', '87.1', 'Morphisms of ringed spaces via sections', 1),
('87', '87.2', 'Morphisms of ringed spaces via stalks', 2),
('87', '87.3', 'Morphisms of locally ringed spaces', 3),
('87', '87.4', 'A few examples of morphisms between affine schemes', 4),
('87', '87.5', 'The big theorem', 5),
('87', '87.6', 'More examples of scheme morphisms', 6),
('87', '87.7', 'A little bit on non-affine schemes', 7),
('87', '87.8', 'Where to go from here', 8),
('87', '87.9', 'A few harder problems to think about', 9),
('88', '88.1', 'Let''s construct a monster', 1),
('88', '88.2', 'Review of finite induction', 2),
('88', '88.3', 'Transfinite induction', 3),
('88', '88.4', 'Wrapping up functional equations', 4),
('88', '88.5', 'Zorn''s lemma', 5),
('88', '88.6', 'A few harder problems to think about', 6),
('89', '89.1', 'The ultimate functional equation', 1),
('89', '89.2', 'Cantor''s paradox', 2),
('89', '89.3', 'The language of set theory', 3),
('89', '89.4', 'The axioms of ZFC', 4),
('89', '89.5', 'Encoding', 5),
('89', '89.6', 'Choice and well-ordering', 6),
('89', '89.7', 'Sets vs classes', 7),
('89', '89.8', 'A few harder problems to think about', 8),
('90', '90.1', 'Counting for preschoolers', 1),
('90', '90.2', 'Counting for set theorists', 2),
('90', '90.3', 'Definition of an ordinal', 3),
('90', '90.4', 'Ordinals are ``tall''''', 4),
('90', '90.5', 'Transfinite induction and recursion', 5),
('90', '90.6', 'Ordinal arithmetic', 6),
('90', '90.7', 'The hierarchy of sets', 7),
('90', '90.8', 'A few harder problems to think about', 8),
('91', '91.1', 'Equinumerous sets and cardinals', 1),
('91', '91.2', 'Cardinalities', 2),
('91', '91.3', 'Aleph numbers', 3),
('91', '91.4', 'Cardinal arithmetic', 4),
('91', '91.5', 'Cardinal exponentiation', 5),
('91', '91.6', 'Cofinality', 6),
('91', '91.7', 'Inaccessible cardinals', 7),
('91', '91.8', 'A few harder problems to think about', 8),
('92', '92.1', 'Models', 1),
('92', '92.2', 'Sentences and satisfaction', 2),
('92', '92.3', 'The Levy hierarchy', 3),
('92', '92.4', 'Substructures, and Tarski-Vaught', 4),
('92', '92.5', 'Obtaining the axioms of ZFC', 5),
('92', '92.6', 'Mostowski collapse', 6),
('92', '92.7', 'Adding an inaccessible', 7),
('92', '92.8', 'FAQ''s on countable models', 8),
('92', '92.9', 'Picturing inner models', 9),
('92', '92.10', 'A few harder problems to think about', 10),
('93', '93.1', 'Setting up posets', 1),
('93', '93.2', 'More properties of posets', 2),
('93', '93.3', 'Names, and the generic extension', 3),
('93', '93.4', 'Fundamental theorem of forcing', 4),
('93', '93.5', '(Optional) Defining the relation', 5),
('93', '93.6', 'The remaining axioms', 6),
('93', '93.7', 'A few harder problems to think about', 7),
('94', '94.1', 'Adding in reals', 1),
('94', '94.2', 'The countable chain condition', 2),
('94', '94.3', 'Preserving cardinals', 3),
('94', '94.4', 'Infinite combinatorics', 4),
('94', '94.5', 'A few harder problems to think about', 5),
('A', 'A.1', 'Basic algebra and topology', 1),
('A', 'A.2', 'Second-year topics', 2),
('A', 'A.3', 'Advanced topics', 3),
('A', 'A.4', 'Topics not in Napkin', 4),
('D', 'D.1', 'General', 1),
('D', 'D.2', 'Functions and sets', 2),
('D', 'D.3', 'Abstract and linear algebra', 3),
('D', 'D.4', 'Quantum computation', 4),
('D', 'D.5', 'Topology and real/complex analysis', 5),
('D', 'D.6', 'Measure theory and probability', 6),
('D', 'D.7', 'Algebraic topology', 7),
('D', 'D.8', 'Category theory', 8),
('D', 'D.9', 'Differential geometry', 9),
('D', 'D.10', 'Algebraic number theory', 10),
('D', 'D.11', 'Representation theory', 11),
('D', 'D.12', 'Algebraic geometry', 12),
('D', 'D.13', 'Set theory', 13),
('E', 'E.1', 'Sets', 1),
('E', 'E.2', 'Functions', 2),
('E', 'E.3', 'Equivalence relations', 3)
) AS v(chapter, key, title, ord) JOIN "math"."chapters" c ON c.key = v.chapter;
--> statement-breakpoint
-- Progress bookkeeping, so a plain UPDATE from a bot or psql behaves like the site:
-- status changes stamp started_at/completed_at, and every status, exercise, note and section
-- change is logged to math.activity (source from the transaction-local math.source setting,
-- 'sql' when unset; math.apply_progress sets it).
CREATE FUNCTION "math"."chapters_stamp"() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.status IS DISTINCT FROM OLD.status THEN
IF NEW.status = 'not_started' THEN
NEW.started_at := NULL;
NEW.completed_at := NULL;
ELSE
IF NEW.status IN ('reading', 'exercises', 'done') AND NEW.started_at IS NULL THEN
NEW.started_at := now();
END IF;
IF NEW.status = 'done' THEN
NEW.completed_at := coalesce(NEW.completed_at, now());
ELSE
NEW.completed_at := NULL;
END IF;
END IF;
END IF;
NEW.updated_at := now();
RETURN NEW;
END $$;
--> statement-breakpoint
CREATE FUNCTION "math"."chapters_log"() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
src text := coalesce(nullif(current_setting('math.source', true), ''), 'sql');
BEGIN
IF NEW.status IS DISTINCT FROM OLD.status THEN
INSERT INTO math.activity (chapter_id, kind, from_value, to_value, source)
VALUES (NEW.id, 'status', OLD.status, NEW.status, src);
END IF;
IF NEW.exercises_done IS DISTINCT FROM OLD.exercises_done THEN
INSERT INTO math.activity (chapter_id, kind, from_value, to_value, delta, source)
VALUES (NEW.id, 'exercises', OLD.exercises_done::text, NEW.exercises_done::text,
NEW.exercises_done - OLD.exercises_done, src);
END IF;
IF coalesce(NEW.notes, '') IS DISTINCT FROM coalesce(OLD.notes, '') THEN
INSERT INTO math.activity (chapter_id, kind, to_value, source)
VALUES (NEW.id, 'note', left(NEW.notes, 200), src);
END IF;
RETURN NULL;
END $$;
--> statement-breakpoint
CREATE FUNCTION "math"."sections_log"() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF (NEW.done_at IS NULL) IS DISTINCT FROM (OLD.done_at IS NULL) THEN
INSERT INTO math.activity (chapter_id, section_id, kind, to_value, source)
VALUES (NEW.chapter_id, NEW.id, 'section', CASE WHEN NEW.done_at IS NULL THEN 'undone' ELSE 'done' END,
coalesce(nullif(current_setting('math.source', true), ''), 'sql'));
END IF;
RETURN NULL;
END $$;
--> statement-breakpoint
CREATE TRIGGER "chapters_stamp" BEFORE UPDATE ON "math"."chapters"
FOR EACH ROW EXECUTE FUNCTION "math"."chapters_stamp"();
--> statement-breakpoint
CREATE TRIGGER "chapters_log" AFTER UPDATE ON "math"."chapters"
FOR EACH ROW EXECUTE FUNCTION "math"."chapters_log"();
--> statement-breakpoint
CREATE TRIGGER "sections_log" AFTER UPDATE OF "done_at" ON "math"."sections"
FOR EACH ROW EXECUTE FUNCTION "math"."sections_log"();
--> statement-breakpoint
-- One progress update as JSON (or an array of them), the format bots send:
-- {"chapter": "3", "status": "reading" | "exercises" | "done" | "skipped" | "not_started",
-- "exercises_done": 4 (absolute) or "exercises_add": 1 (relative),
-- "notes": "short note" ("" clears),
-- "sections_done": ["3.1", "3.2"] or "all", "sections_undone": [...] or "all",
-- "source": "bot"}
-- Every field but "chapter" is optional. Returns the updated chapter(s) as JSON.
CREATE FUNCTION "math"."apply_progress"(p jsonb) RETURNS jsonb LANGUAGE plpgsql AS $$
DECLARE
ch_id integer;
k text;
item jsonb;
res jsonb := '[]'::jsonb;
BEGIN
IF jsonb_typeof(p) = 'array' THEN
FOR item IN SELECT * FROM jsonb_array_elements(p) LOOP
res := res || jsonb_build_array(math.apply_progress(item));
END LOOP;
RETURN res;
END IF;
k := btrim(p->>'chapter');
SELECT id INTO ch_id FROM math.chapters WHERE key = upper(k);
IF ch_id IS NULL THEN
RAISE EXCEPTION 'math.apply_progress: unknown chapter %', coalesce(k, '(missing)');
END IF;
PERFORM set_config('math.source', coalesce(nullif(p->>'source', ''), 'bot'), true);
UPDATE math.chapters SET
status = coalesce(nullif(p->>'status', ''), status),
exercises_done = CASE
WHEN p ? 'exercises_done' THEN greatest(0, (p->>'exercises_done')::int)
WHEN p ? 'exercises_add' THEN greatest(0, exercises_done + (p->>'exercises_add')::int)
ELSE exercises_done END,
notes = CASE WHEN p ? 'notes' THEN nullif(btrim(p->>'notes'), '') ELSE notes END
WHERE id = ch_id;
IF p ? 'sections_done' THEN
UPDATE math.sections SET done_at = now()
WHERE chapter_id = ch_id AND done_at IS NULL
AND (p->'sections_done' = '"all"'::jsonb
OR (jsonb_typeof(p->'sections_done') = 'array'
AND key IN (SELECT jsonb_array_elements_text(p->'sections_done'))));
END IF;
IF p ? 'sections_undone' THEN
UPDATE math.sections SET done_at = NULL
WHERE chapter_id = ch_id AND done_at IS NOT NULL
AND (p->'sections_undone' = '"all"'::jsonb
OR (jsonb_typeof(p->'sections_undone') = 'array'
AND key IN (SELECT jsonb_array_elements_text(p->'sections_undone'))));
END IF;
SELECT jsonb_build_object(
'chapter', c.key, 'title', c.title, 'status', c.status,
'exercises_done', c.exercises_done, 'exercise_count', c.exercise_count,
'started_at', c.started_at, 'completed_at', c.completed_at,
'sections_done', (SELECT count(*) FROM math.sections s WHERE s.chapter_id = c.id AND s.done_at IS NOT NULL),
'sections', (SELECT count(*) FROM math.sections s WHERE s.chapter_id = c.id))
INTO item FROM math.chapters c WHERE c.id = ch_id;
RETURN item;
END $$;
--> statement-breakpoint
-- Privacy: private by default (owner-only on the site, MATH_PRIVATE in #lib/math). Like notes
-- and tasks, only the app role can read or write (no personal_ro). Grants only run on prod
-- (migrations as `personal`).
REVOKE ALL ON SCHEMA math FROM PUBLIC;
--> statement-breakpoint
DO $$
BEGIN
IF current_user = 'personal' AND EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN
GRANT USAGE ON SCHEMA math TO personal_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA math TO personal_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA math TO personal_app;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA math TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA math
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA math
GRANT USAGE, SELECT ON SEQUENCES TO personal_app;
END IF;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -287,6 +287,33 @@ try {
notes.public_usage notes.public_usage
) )
throw new Error(`notes schema tables/grants are wrong: ${JSON.stringify(notes)}`); throw new Error(`notes schema tables/grants are wrong: ${JSON.stringify(notes)}`);
// Math (the Napkin tracker, private by default): app role read/write, personal_ro none.
// 4 = parts, chapters, sections, activity; the migration seeds all 100 chapters.
const MATH_TABLES = 4;
const [math] = await client`
select
count(*) filter (where has_table_privilege(c.oid, 'select, insert, update, delete')) as app_rw,
count(*) filter (
where exists (
select 1 from aclexplode(c.relacl) a join pg_roles r on r.oid = a.grantee
where r.rolname = 'personal_ro'
)
) as ro_read,
count(*) as tables,
has_schema_privilege('public', 'math', 'usage') as public_usage
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'math' and c.relkind = 'r'
`;
if (
Number(math.tables) !== MATH_TABLES ||
Number(math.app_rw) !== MATH_TABLES ||
Number(math.ro_read) !== 0 ||
math.public_usage
)
throw new Error(`math schema tables/grants are wrong: ${JSON.stringify(math)}`);
const [{ n: mathChapters }] = await client`select count(*)::int as n from math.chapters`;
if (mathChapters !== 100)
throw new Error(`math.chapters has ${mathChapters} rows, expected the 100 Napkin chapters`);
// Energy tracker reads the ned-collector tables (SELECT only). // Energy tracker reads the ned-collector tables (SELECT only).
const [energy] = await client` const [energy] = await client`
select count(*) as hours, max(valid_from) as latest from energy.ned_utilization select count(*) as hours, max(valid_from) as latest from energy.ned_utilization
@@ -296,7 +323,7 @@ try {
if (!Number(energy.hours)) if (!Number(energy.hours))
console.warn('Warning: energy.ned_utilization has no hourly data from the last 7 days'); console.warn('Warning: energy.ned_utilization has no hourly data from the last 7 days');
console.log( console.log(
`health: ${health.tables} tables, tasks: ${tasks.tables} tables, groceries: ${groceries.tables} tables, stats: ${stats.tables} tables, ai: ${ai.tables} tables, movies: ${movies.tables} tables, worldtodo: ${worldtodo.tables} tables (${worldtodoTasks} tasks), notes: ${notes.tables} tables, app read/write, personal_ro none; energy: ${energy.hours} hourly rows, latest ${energy.latest ? new Date(energy.latest).toISOString() : 'none'}` `health: ${health.tables} tables, tasks: ${tasks.tables} tables, groceries: ${groceries.tables} tables, stats: ${stats.tables} tables, ai: ${ai.tables} tables, movies: ${movies.tables} tables, worldtodo: ${worldtodo.tables} tables (${worldtodoTasks} tasks), notes: ${notes.tables} tables, math: ${math.tables} tables (${mathChapters} chapters), app read/write, personal_ro none; energy: ${energy.hours} hourly rows, latest ${energy.latest ? new Date(energy.latest).toISOString() : 'none'}`
); );
const [catalog] = await client` const [catalog] = await client`
select select

39
src/lib/math.ts Normal file
View File

@@ -0,0 +1,39 @@
// Math tracker (/trackers/math): Evan Chen's "An Infinitely Large Napkin". Shared by the
// server (owner gate, loads) and the page.
/**
* The privacy switch. true: owner-only (404 for everyone else, lock icon in every tracker
* list). false: a public, read-only page for everyone else (notes stay owner-only, edits
* are always owner-only). Flip this one constant; owner.ts and projects.ts read it.
*/
export const MATH_PRIVATE = true;
export const MATH_STATUSES = ['not_started', 'reading', 'exercises', 'done', 'skipped'] as const;
export type MathStatus = (typeof MATH_STATUSES)[number];
export const STATUS_LABEL: Record<MathStatus, string> = {
not_started: 'not started',
reading: 'reading',
exercises: 'exercises',
done: 'done',
skipped: 'skipped'
};
/** The free PDF of the book; chapters link to it with #page=N (PDF page numbers). */
export const NAPKIN_PDF = 'https://venhance.github.io/napkin/Napkin.pdf';
export const pdfPage = (page: number) => `${NAPKIN_PDF}#page=${page}`;
/** "9,10,11,12,18" → "9–12, 18" (numeric keys only; letters are listed as-is). */
export function compactKeys(keys: readonly string[]): string {
const nums = keys.filter((k) => /^\d+$/.test(k)).map(Number);
const other = keys.filter((k) => !/^\d+$/.test(k));
nums.sort((a, b) => a - b);
const out: string[] = [];
for (let i = 0; i < nums.length;) {
let j = i;
while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++;
out.push(j - i >= 2 ? `${nums[i]}–${nums[j]}` : nums.slice(i, j + 1).join(', '));
i = j + 1;
}
return [...out, ...other].join(', ');
}

View File

@@ -1,3 +1,4 @@
import { MATH_PRIVATE } from './math';
// Real projects (tools and experiments). Personal data trackers live in `trackers`. // Real projects (tools and experiments). Personal data trackers live in `trackers`.
export const projects = [ export const projects = [
{ {
@@ -386,6 +387,13 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [
external: false, external: false,
private: true private: true
}, },
{
name: 'math',
subtitle: "working through Evan Chen's Napkin: chapters, exercises and notes",
href: '/trackers/math',
external: false,
private: MATH_PRIVATE
},
{ {
name: 'health', name: 'health',
subtitle: 'body, workouts, sleep, meds and vaccinations', subtitle: 'body, workouts, sleep, meds and vaccinations',

View File

@@ -33,3 +33,4 @@ export * from './movies';
export * from './worldtodo'; export * from './worldtodo';
export * from './osu-goals'; export * from './osu-goals';
export * from './notes'; export * from './notes';
export * from './math';

View File

@@ -0,0 +1,112 @@
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')`)
]
);

View File

@@ -29,3 +29,4 @@ export const aiSchema = pgSchema('ai');
export const moviesSchema = pgSchema('movies'); export const moviesSchema = pgSchema('movies');
export const worldtodoSchema = pgSchema('worldtodo'); export const worldtodoSchema = pgSchema('worldtodo');
export const notesSchema = pgSchema('notes'); export const notesSchema = pgSchema('notes');
export const mathSchema = pgSchema('math');

134
src/lib/server/math.ts Normal file
View File

@@ -0,0 +1,134 @@
// Math tracker (/trackers/math): loads the Napkin outline with progress, and applies progress
// updates through math.apply_progress (the same JSON format bots use, see the migration).
import { client } from '#lib/server/db/index.js';
import { MATH_STATUSES, type MathStatus } from '#lib/math.js';
import * as z from 'zod';
export type MathSection = { id: number; key: string; title: string; done: boolean };
export type MathChapter = {
id: number;
key: string;
partKey: string;
title: string;
pageStart: number;
pageEnd: number;
exerciseCount: number | null;
prereqs: string[];
status: MathStatus;
exercisesDone: number;
notes: string | null;
startedAt: string | null;
completedAt: string | null;
updatedAt: string;
sections: MathSection[];
};
export type MathPart = { key: string; num: number | null; title: string };
export type MathActivity = {
id: number;
chapterKey: string;
chapterTitle: string;
sectionKey: string | null;
kind: 'status' | 'exercises' | 'note' | 'section';
fromValue: string | null;
toValue: string | null;
delta: number | null;
source: string;
at: string;
};
const iso = (d: Date | string | null) => (d ? new Date(d).toISOString() : null);
export async function loadMath(owner: boolean) {
const [parts, chapters, sections, activity] = await Promise.all([
client`SELECT key, num, title FROM math.parts ORDER BY sort_order`,
client`SELECT c.id, c.key, p.key AS part_key, c.title, c.page_start, c.page_end,
c.exercise_count, c.prereqs, c.status, c.exercises_done, c.notes,
c.started_at, c.completed_at, c.updated_at
FROM math.chapters c JOIN math.parts p ON p.id = c.part_id
ORDER BY c.sort_order`,
client`SELECT id, chapter_id, key, title, done_at FROM math.sections
ORDER BY chapter_id, sort_order`,
client`SELECT a.id, c.key AS chapter_key, c.title AS chapter_title, s.key AS section_key,
a.kind, a.from_value, a.to_value, a.delta, a.source, a.created_at
FROM math.activity a
JOIN math.chapters c ON c.id = a.chapter_id
LEFT JOIN math.sections s ON s.id = a.section_id
ORDER BY a.created_at DESC, a.id DESC LIMIT 25`
]);
const byChapter = new Map<number, MathSection[]>();
for (const s of sections) {
const list = byChapter.get(s.chapter_id) ?? [];
list.push({ id: s.id, key: s.key, title: s.title, done: !!s.done_at });
byChapter.set(s.chapter_id, list);
}
return {
parts: parts.map((p) => ({ key: p.key, num: p.num, title: p.title }) as MathPart),
chapters: chapters.map((c): MathChapter => ({
id: c.id,
key: c.key,
partKey: c.part_key,
title: c.title,
pageStart: c.page_start,
pageEnd: c.page_end,
exerciseCount: c.exercise_count,
prereqs: c.prereqs ?? [],
status: c.status,
exercisesDone: c.exercises_done,
notes: owner ? c.notes : null,
startedAt: iso(c.started_at),
completedAt: iso(c.completed_at),
updatedAt: iso(c.updated_at)!,
sections: byChapter.get(c.id) ?? []
})),
activity: activity
.filter((a) => owner || a.kind !== 'note')
.map((a): MathActivity => ({
id: a.id,
chapterKey: a.chapter_key,
chapterTitle: a.chapter_title,
sectionKey: a.section_key,
kind: a.kind,
fromValue: a.kind === 'note' && !owner ? null : a.from_value,
toValue: a.kind === 'note' && !owner ? null : a.to_value,
delta: a.delta,
source: a.source,
at: iso(a.created_at)!
}))
};
}
const progressInput = z
.object({
chapter: z.string().trim().min(1).max(4),
status: z.enum(MATH_STATUSES).optional(),
exercises_add: z.coerce.number().int().min(-50).max(50).optional(),
exercises_done: z.coerce.number().int().min(0).max(500).optional(),
notes: z.string().max(2000).optional(),
section: z.string().trim().max(12).optional(),
section_done: z.enum(['1', '0']).optional()
})
.strict();
/** One owner update from the page (form fields), applied as a math.apply_progress JSON. */
export async function applyFromForm(form: FormData) {
const raw: Record<string, string> = {};
for (const [k, v] of form) if (typeof v === 'string' && v !== '') raw[k] = v;
if (form.has('notes')) raw.notes = String(form.get('notes') ?? '');
const parsed = progressInput.safeParse(raw);
if (!parsed.success) return { ok: false as const, message: 'Invalid update' };
const p = parsed.data;
const json: Record<string, unknown> = { chapter: p.chapter, source: 'site' };
if (p.status) json.status = p.status;
if (p.exercises_add !== undefined) json.exercises_add = p.exercises_add;
if (p.exercises_done !== undefined) json.exercises_done = p.exercises_done;
if (p.notes !== undefined) json.notes = p.notes;
if (p.section) json[p.section_done === '0' ? 'sections_undone' : 'sections_done'] = [p.section];
try {
await client`SELECT math.apply_progress(${JSON.stringify(json)}::jsonb)`;
return { ok: true as const };
} catch (e) {
const msg = e instanceof Error ? e.message : '';
if (msg.includes('unknown chapter')) return { ok: false as const, message: 'Unknown chapter' };
throw e;
}
}

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { isOwner, isOwnerOnlyPath, isOwnerWritePath } from './owner'; import { isOwner, isOwnerOnlyPath, isOwnerWritePath } from './owner';
import { MATH_PRIVATE } from '#lib/math.js';
const event = (user: Partial<NonNullable<App.Locals['user']>> | null) => const event = (user: Partial<NonNullable<App.Locals['user']>> | null) =>
({ locals: { user, session: null } }) as unknown as { locals: App.Locals }; ({ locals: { user, session: null } }) as unknown as { locals: App.Locals };
@@ -37,6 +38,11 @@ describe('owner gate', () => {
expect(isOwnerOnlyPath('/trackers/books/covers/12')).toBe(false); expect(isOwnerOnlyPath('/trackers/books/covers/12')).toBe(false);
expect(isOwnerOnlyPath('/trackers/books/12/reader-notes-public')).toBe(false); expect(isOwnerOnlyPath('/trackers/books/12/reader-notes-public')).toBe(false);
}); });
it('gates the math tracker while MATH_PRIVATE is on; writes are always owner-only', () => {
expect(isOwnerOnlyPath('/trackers/math')).toBe(MATH_PRIVATE);
expect(isOwnerOnlyPath('/trackers/math/__data.json')).toBe(MATH_PRIVATE);
expect(isOwnerWritePath('/trackers/math')).toBe(true);
});
it('only lets the owner write in owner areas', () => { it('only lets the owner write in owner areas', () => {
expect(isOwnerWritePath('/trackers/books/12')).toBe(true); expect(isOwnerWritePath('/trackers/books/12')).toBe(true);
expect(isOwnerWritePath('/trackers/books')).toBe(true); expect(isOwnerWritePath('/trackers/books')).toBe(true);

View File

@@ -1,4 +1,5 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { MATH_PRIVATE } from '#lib/math.js';
// The site has exactly one owner: Stan's account (prod auth.user id tfn6OZDOpiQvMARIH65HQ2XJuaVvmGEe). // The site has exactly one owner: Stan's account (prod auth.user id tfn6OZDOpiQvMARIH65HQ2XJuaVvmGEe).
// Owner-only data, such as the health tracker, is gated on that account's email, not on // Owner-only data, such as the health tracker, is gated on that account's email, not on
@@ -38,7 +39,9 @@ export const ownerOnlyPrefixes = [
'/api/spotify', '/api/spotify',
// Worldwide todo list: the list is public, the review queue and its API are mine only. // Worldwide todo list: the list is public, the review queue and its API are mine only.
'/projects/worldwide-todo/queue', '/projects/worldwide-todo/queue',
'/projects/worldwide-todo/api/queue' '/projects/worldwide-todo/api/queue',
// Math tracker (the Napkin): owner-only while MATH_PRIVATE (#lib/math) is on.
...(MATH_PRIVATE ? (['/trackers/math'] as const) : [])
] as const; ] as const;
/** /**
@@ -67,7 +70,8 @@ export const ownerWritePrefixes = [
'/trackers/pc', '/trackers/pc',
'/trackers/notes', '/trackers/notes',
'/trackers/ai', '/trackers/ai',
'/trackers/movies' '/trackers/movies',
'/trackers/math'
] as const; ] as const;
const normalise = (pathname: string) => pathname.toLowerCase().replace(/\/+$/, ''); const normalise = (pathname: string) => pathname.toLowerCase().replace(/\/+$/, '');

View File

@@ -96,6 +96,13 @@ export const trackerIcons: Record<string, TrackerIcon> = {
'M20 3v4M22 5h-4M4 17v2M5 18H3' 'M20 3v4M22 5h-4M4 17v2M5 18H3'
] ]
}, },
math: {
from: '#818cf8',
to: '#4338ca',
ink: WHITE,
// sigma
paths: ['M18 7V4H6l6 8-6 8h12v-3']
},
notes: { notes: {
from: '#fde68a', from: '#fde68a',
to: '#f59e0b', to: '#f59e0b',

View File

@@ -0,0 +1,33 @@
import { MATH_PRIVATE } from '#lib/math.js';
import { applyFromForm, loadMath } from '#lib/server/math.js';
import { isOwner, requireOwner } from '#lib/server/owner.js';
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
// Math tracker: Evan Chen's "An Infinitely Large Napkin". Private by default (MATH_PRIVATE in
// #lib/math): then everyone else gets a 404 for the page and its __data.json, and
// hooks.server.ts 404s the path as a backstop. Edits are always owner-only.
export const load: PageServerLoad = async (event) => {
if (MATH_PRIVATE) requireOwner(event);
const owner = isOwner(event);
if (owner) event.setHeaders({ 'cache-control': 'private, no-store' });
return {
owner,
private: MATH_PRIVATE,
now: Date.now(),
...(await loadMath(owner)),
seo: {
title: 'Math tracker',
description: "Working through Evan Chen's An Infinitely Large Napkin, chapter by chapter.",
index: !MATH_PRIVATE
}
};
};
export const actions: Actions = {
progress: async (event) => {
requireOwner(event);
const r = await applyFromForm(await event.request.formData());
return r.ok ? { ok: true } : fail(400, { message: r.message });
}
};

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB