From b44e20be25158da7031eb26e5966042b50a400e9 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Tue, 8 Sep 2026 16:41:33 -0400 Subject: [PATCH 01/14] Drop three test assertions that cannot fail - load.test.ts "sorts by date descending" asserted a one-element array equals itself sorted; the fixture dir holds a single episode. The ordering contract is already pinned directly by the episodeOrder tie-break test below it. - load.test.ts's tie-break test carried a redundant re-import and a trailing assert.ok(await load()) unrelated to what it tests. - srt.test.ts "the last cue runs five seconds" asserted two substrings of the exact SRT string pinned by the test immediately above it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk --- tests/load.test.ts | 7 ------- tests/srt.test.ts | 6 ------ 2 files changed, 13 deletions(-) diff --git a/tests/load.test.ts b/tests/load.test.ts index 3a7657f..91aeb1b 100644 --- a/tests/load.test.ts +++ b/tests/load.test.ts @@ -13,18 +13,12 @@ test("loadEpisodes reads and parses the content directory", async () => { assert.equal(episodes[0].sections.length, 2); }); -test("loadEpisodes sorts by date descending", async () => { - const dates = (await loadEpisodes()).map((e) => e.date); - assert.deepEqual(dates, [...dates].sort().reverse()); -}); - test("getEpisode finds by slug and misses cleanly", async () => { assert.equal((await getEpisode("2026-05"))?.slug, "2026-05"); assert.equal(await getEpisode("1999-01"), undefined); }); test("loadEpisodes breaks a date tie by slug descending", async () => { - const { loadEpisodes: load } = await import("../src/content/load.ts"); const tied = [ { slug: "2026-06", date: "2026-06-24" }, { slug: "2026-07", date: "2026-06-24" }, @@ -34,5 +28,4 @@ test("loadEpisodes breaks a date tie by slug descending", async () => { tied.sort(episodeOrder).map((e) => e.slug), ["2026-07", "2026-06", "2026-05"], ); - assert.ok(await load()); }); diff --git a/tests/srt.test.ts b/tests/srt.test.ts index 891e4da..4d2085b 100644 --- a/tests/srt.test.ts +++ b/tests/srt.test.ts @@ -37,9 +37,3 @@ test("toSrt numbers cues, formats times, and prefixes the speaker", () => { ].join("\n"), ); }); - -test("the last cue runs five seconds", () => { - const srt = toSrt(parseEpisode(raw, "2026-05")); - assert.ok(srt.trimEnd().endsWith("Carl Vitullo: But yeah, okay, into some new releases.")); - assert.ok(srt.includes("00:01:49,000 --> 00:01:54,000")); -}); From 2ef4b954ee8b771bad2a32acd222a8db255569fe Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Tue, 8 Sep 2026 16:43:54 -0400 Subject: [PATCH 02/14] Delete the one-time migration script and its tests scripts/migrate.ts converted the historical reactiflux.com transcripts into content/episodes/. That conversion has run; the output is committed and is now the source of truth. Nothing imports the script, and it has no package.json entry. Removes the script, tests/migrate.test.ts, and the seven legacy-*.md fixtures used only by it. src/content/headings.ts and scripts/publish-transcript.ts keep insertHeadings, which publish-transcript still uses; their doc comments no longer point at migrate. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk --- scripts/migrate.ts | 338 ---------------------------- scripts/publish-transcript.ts | 3 +- src/content/headings.ts | 5 +- tests/fixtures/legacy-2023-10.md | 21 -- tests/fixtures/legacy-2023-march.md | 24 -- tests/fixtures/legacy-2024-03.md | 33 --- tests/fixtures/legacy-2025-01.md | 34 --- tests/fixtures/legacy-2025-06.md | 37 --- tests/fixtures/legacy-2025-09.md | 30 --- tests/fixtures/legacy-2026-05.md | 39 ---- tests/migrate.test.ts | 307 ------------------------- 11 files changed, 3 insertions(+), 868 deletions(-) delete mode 100644 scripts/migrate.ts delete mode 100644 tests/fixtures/legacy-2023-10.md delete mode 100644 tests/fixtures/legacy-2023-march.md delete mode 100644 tests/fixtures/legacy-2024-03.md delete mode 100644 tests/fixtures/legacy-2025-01.md delete mode 100644 tests/fixtures/legacy-2025-06.md delete mode 100644 tests/fixtures/legacy-2025-09.md delete mode 100644 tests/fixtures/legacy-2026-05.md delete mode 100644 tests/migrate.test.ts diff --git a/scripts/migrate.ts b/scripts/migrate.ts deleted file mode 100644 index 1bc6621..0000000 --- a/scripts/migrate.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; -import { join, resolve } from "node:path"; -import { flattenLinks, normalizeTime, slug } from "../src/content/slug.ts"; -import { splitFile } from "../src/content/parse.ts"; -import { insertHeadings } from "../src/content/headings.ts"; -import { serializeEpisodeFile } from "../src/content/serialize.ts"; - -const MONTHS = [ - "january", "february", "march", "april", "may", "june", - "july", "august", "september", "october", "november", "december", -]; - -/** "tmir-2024-03.md" | "tmir-march-2023.md" | "tmir-dec-2023.md" -> "yyyy-mm" */ -export function slugFromFilename(name: string): string | null { - const iso = /^tmir-(\d{4})-(\d{2})\.md$/.exec(name); - if (iso) return `${iso[1]}-${iso[2]}`; - - const named = /^tmir-([a-z]+)-(\d{4})\.md$/.exec(name); - if (!named) return null; - const idx = MONTHS.findIndex((m) => m.startsWith(named[1])); - if (idx === -1) return null; - return `${named[2]}-${String(idx + 1).padStart(2, "0")}`; -} - -const SPEAKER_PARA = /^(?:\[\d{1,3}(?::\d{2}){1,2}\]\s*)?\*\*[^*]+:\*\*/; -// Unbolded speaker prefix, e.g. `Carl: ` or a raw Descript id `2-vcarl: ` -// (tmir-2025-09.md, tmir-2025-11.md never wrap the label in `**...**`). -// Matching on shape alone is too loose (`Note: something` looks identical), -// so a match here only counts as a speaker if isKnownSpeakerToken() confirms it. -const UNBOLDED_SPEAKER = /^(?:\[\d{1,3}(?::\d{2}){1,2}\]\s*)?([A-Za-z0-9-]{1,20}):\s+/; -const LEADING_TIME = /^\[(\d{1,3}(?::\d{2}){1,2})\]\s*/; -const TRAILING_TIME = /\s*\[(\d{1,3}(?::\d{2}){1,2})\]\s*$/; -const HEADING_TIME = /^(#{1,2})\s*\[(\d{1,3}(?::\d{2}){1,2})\]\s*(.*)$/; -const OUTLINE_LINE = /^(\s*)- (.+)$/; -// `- [[mm:ss](#anchor)] Title` (linked) or a bare `- [mm:ss] Title` (2023-10, -// 2023-11 have no anchor link at all). Both may sit behind a short prefix -// like `⚡️ ` (2024-04's lightning-round bullets); PREFIX captures that. -const PREFIX = /^([^[]{1,4})(?=\[)/; -const OUTLINE_TIME_LINKED = /^\[\[(\d{1,3}(?::\d{2}){1,2})\]\(#[^)]*\)\]\s*/; -const OUTLINE_TIME_BARE = /^\[(\d{1,3}(?::\d{2}){1,2})\]\s*/; - -// Short first-name / raw-Descript-id -> full name, for the unbolded-speaker -// files where the label isn't the canonical "Carl Vitullo" form the bolded -// files use. Descript ids look like `1-vcarl`; strip the leading `N-`. -const NAME_MAP: Record = { - carl: "Carl Vitullo", - vcarl: "Carl Vitullo", - mark: "Mark Erikson", - acemarke: "Mark Erikson", - mo: "Mo Javad", - mojavad: "Mo Javad", -}; - -/** - * Canonicalize a speaker label, bolded or not. A trailing parenthetical is an - * editing note, not part of the name (`Carl Vitullo (editing)`), so it's - * dropped before the lookup. Bare `Mo` is Mo Javad except in the files where - * Mo Khazali is the guest — there the short form is his, so leave it alone. - * Anything not in the map (guests) keeps the name it already has. - */ -export function normalizeSpeakerName(raw: string, boldedNames?: Set): string { - const name = raw.replace(/\s*\([^)]*\)\s*$/, "").trim(); - const key = name.replace(/^\d+-/, "").toLowerCase(); - if (key === "mo" && boldedNames?.has("mo khazali")) return name; - return NAME_MAP[key] ?? name; -} - -/** Every name (and its first word) that appears bolded as a speaker anywhere in the file. */ -function collectBoldedSpeakers(body: string): Set { - const names = new Set(); - for (const m of body.matchAll(/\*\*([^*]+):\*\*/g)) { - const name = m[1].trim().toLowerCase(); - names.add(name); - names.add(name.split(/\s+/)[0]); - } - return names; -} - -/** Is `raw` (an UNBOLDED_SPEAKER match) an actual known speaker, not just `Word:` prose? */ -function isKnownSpeakerToken(raw: string, boldedNames: Set): boolean { - const key = raw.replace(/^\d+-/, "").toLowerCase(); - return key in NAME_MAP || boldedNames.has(key); -} - -/** Remove and blocks. */ -function stripEmbeds(body: string): string { - return body - .replace(/ - - - -- [[01:29](#job-market-fred-data-layoffsfyi)] Job market: [FRED data](https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE), [Layoffs.fyi](https://layoffs.fyi/) -- [[04:17](#new-releases)] New releases - - [[04:26](#react-query-563)] [React query 5.63](https://bsky.app/profile/tkdodo.eu/post/3lfaeteulds2i) - -**Mark:** am making the news. [01:26] - -**Carl:** making the news and then reporting on it. Cool. - -## Job market: [FRED data](https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE), [Layoffs.fyi](https://layoffs.fyi/) - -**Carl:** Yeah, let's start off briefly, overview of the job market. [01:50] - -But conferences still don't have a ton of queued up ones. diff --git a/tests/fixtures/legacy-2025-06.md b/tests/fixtures/legacy-2025-06.md deleted file mode 100644 index 9ed488f..0000000 --- a/tests/fixtures/legacy-2025-06.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "TMiR 2025-06: kinda quiet tbh. ES2025 finalized, new Safari" -date: 2025-06-30 -time: 10am PT / 5PM GMT -location: Main Stage on Reactiflux -description: "Join Carl, Mark, and Mo as we break down This Month in React." -people: "[Carl](https://twitter.com/vcarl_), [Mark](https://twitter.com/acemarke), and [Mo](https://twitter.com/mo__javad)" ---- - - - - - -We had some problems with transcript generation this month, downstream of the audio recording issues. Sorry! Fixed for July. - -- Job market [FRED data](https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE), [Layoffs.fyi](https://layoffs.fyi/) -- New releases - - [Recharts v3.0](https://github.com/recharts/recharts/releases/tag/v3.0.0) - -# This Month in React: June, 2025 - -[00:00] **1-vcarl:** Hi, Carl here. Just a quick note before we get into this episode. - -[00:51] **1-vcarl:** Endlessly! - -## [00:52] job market - -[00:52] **1-vcarl:** Start off with a little bit of job market news. - -## [02:19] New Releases - -[02:19] **1-vcarl:** Yeah. New releases. diff --git a/tests/fixtures/legacy-2025-09.md b/tests/fixtures/legacy-2025-09.md deleted file mode 100644 index f42095a..0000000 --- a/tests/fixtures/legacy-2025-09.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: "TMiR 2025-09: React 19.2 on the horizon; npm is still getting compromised" -date: 2025-09-27 -time: 10am PT / 5PM GMT -location: Main Stage on Reactiflux -description: "Join Carl, Mark, and Mo as we break down This Month in React." -people: "[Carl](https://twitter.com/vcarl_), [Mark](https://twitter.com/acemarke), and [Mo](https://twitter.com/mo__javad)" ---- - - - -- [[00:52](#new-releases)] New releases - - [[00:54](#tanstack-start-10-rc)] [TanStack Start 1.0 RC!](https://tanstack.com/blog/announcing-tanstack-start-v1) - -Carl: Alright. Hello everyone. Thank you for joining us for the September edition of this Month In React. [00:00] - -Mark: Hi, I'm Mark. My day job is working at Replay.io. [00:23] - -Mo: And my name is Mo. [00:37] - -Carl: Yeah, more on that later. [00:51] - -## New releases - -Carl: Let's jump into some new releases. [00:52] diff --git a/tests/fixtures/legacy-2026-05.md b/tests/fixtures/legacy-2026-05.md deleted file mode 100644 index 96d66d9..0000000 --- a/tests/fixtures/legacy-2026-05.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "TMiR 2026-05: Who even is on the Core team anymore, TanStack got pwn'd bad" -date: 2026-05-28 -time: 2pm PT / 9pm GMT -location: Main Stage on Reactiflux -description: "Join Carl Vitullo and Mark Erikson as we discuss what the heck is going on at the React Core team." -people: "[Carl](https://bsky.app/profile/vcarl.com), [Mark](https://bsky.app/profile/acemarke.dev)" ---- - - - - - -- [[00:00:55](#some-podcast-meta)] Some podcast meta -- [[00:01:49](#new-releases)] New Releases - - [[00:01:51](#ts-v7-beta)] [TS v7 beta](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/) -- [[01:08:39](#outro)] Outro - -# Interview - -**Carl Vitullo:** Thank you for joining us. We're coming to you live from Reactiflux. [00:00:00] - -## Some podcast meta - -**Carl Vitullo:** Okay, let's get into it. [00:00:55] - -Stuff has changed. Stuff will continue to change. Just like life. [00:01:40] - -## New Releases - -**Carl Vitullo:** But yeah, okay, into some new releases. [00:01:49] diff --git a/tests/migrate.test.ts b/tests/migrate.test.ts deleted file mode 100644 index 62fff8e..0000000 --- a/tests/migrate.test.ts +++ /dev/null @@ -1,307 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { - convert, - mergeExistingFrontMatter, - normalizeSpeakerName, - slugFromFilename, -} from "../scripts/migrate.ts"; -import { parseEpisode, splitFile } from "../src/content/parse.ts"; - -const fixture = (name: string) => - readFileSync(new URL(`./fixtures/${name}`, import.meta.url), "utf8"); - -test("slugFromFilename maps both filename styles", () => { - assert.equal(slugFromFilename("tmir-2024-03.md"), "2024-03"); - assert.equal(slugFromFilename("tmir-march-2023.md"), "2023-03"); - assert.equal(slugFromFilename("tmir-april-2023.md"), "2023-04"); - assert.equal(slugFromFilename("tmir-dec-2023.md"), "2023-12"); - assert.equal(slugFromFilename("tmir-nov-2023.md"), "2023-11"); - assert.equal(slugFromFilename("tmir-october-2023.md"), "2023-10"); - assert.equal(slugFromFilename("ali-spittel.md"), null); -}); - -test("2023 format: iframe stripped, outline synthesized from headings", () => { - const { text, warnings } = convert(fixture("legacy-2023-march.md")); - assert.ok(!text.includes(" { - const { text } = convert(fixture("legacy-2024-03.md")); - assert.ok(!text.includes("[Spotify](")); - assert.ok(!text.includes(" { - const { text } = convert(fixture("legacy-2025-01.md")); - assert.ok(!text.includes("` before it from 2025-01 on; three files have no iframe at all (`tmir-2025-09.md`, `tmir-dec-2023.md`, `tmir-nov-2023.md`) | -| Pre-outline link list | `- [Spotify](…)` / `- [Apple Podcasts](…)` / `- [RSS](…)` bullets before the iframe (2023–2024) | -| Outline | absent (all of 2023 except dec/nov, plus `tmir-2025-06.md`); `- [[mm:ss](#anchor)] …`; `- [[hh:mm:ss](#anchor)] …` (2026) | -| Transcript start | no marker at all; or an h1 like `# This Month in React: June, 2025` / `# Interview` / `# This Month in React 2023 April` | -| Section headings | `## Title` (most); `# Title` (`tmir-june-2023.md`, five of them); `## [00:52] title` (`tmir-2025-06.md`) | -| Paragraph timestamp | trailing `… [00:16]`; leading `[01:23] **Carl Vitullo:** …` | -| Speaker labels | `**Carl Vitullo:**`, `**Carl:**`, raw Descript ids `**1-vcarl:**` (`tmir-2025-06.md`) | +| Feature | Variants observed | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Header block | `