commit a2f949e945561a0d69ece4aac455a6a6288e54ad
parent db6c5b798b9e2ac78295fa7a1207252d1bdbdbaa
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 6 Oct 2026 16:39:48 -0400
Merge fix/post-slug-from-dir (a renamed social channel's posts read as their new slug)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/common/lib/posts-server.test.ts b/common/lib/posts-server.test.ts
@@ -147,6 +147,26 @@ test("a truncated JSONL line does not poison the rest of its shard", async () =>
});
});
+test("a renamed channel's posts read as the new slug, not the one stamped in the line", async () => {
+ await withTempChannel(async (parent) => {
+ const oldRoot = path.join(parent, "chan");
+ await writePosts(oldRoot, [makePost("p1", "2026-05-01T00:00:00.000Z")]);
+ const { rename } = await import("node:fs/promises");
+ const newRoot = path.join(parent, "chan-renamed");
+ await rename(oldRoot, newRoot);
+
+ // The JSONL still says "chan" — renameChannel does not rewrite it.
+ const raw = await readFile(path.join(newRoot, "posts", "2026-05.jsonl"), "utf8");
+ assert.match(raw, /"channelSlug":"chan"/);
+
+ const [post] = await readPostShard(newRoot, "2026-05");
+ assert.equal(post.channelSlug, "chan-renamed");
+ assert.equal(post.slug, "chan-renamed/p1");
+ const [viaAll] = await readAllPosts(newRoot);
+ assert.equal(viaAll.slug, "chan-renamed/p1");
+ });
+});
+
// ─── deleted-post tracking ───
test("availability merges, and history records only real changes", async () => {
diff --git a/common/lib/posts-server.ts b/common/lib/posts-server.ts
@@ -21,6 +21,7 @@ import {
isPostAvailability,
monthShardFromCreatedAt,
parsePost,
+ postSlug,
type Post,
type PostAvailability,
type PostAvailabilityMap,
@@ -238,6 +239,16 @@ export async function listPostShards(
// Read one month shard. Malformed lines are skipped rather than failing the
// whole shard — an append that was interrupted mid-line must not make every
// earlier post in that month unreadable.
+//
+// THE DIRECTORY IS THE CHANNEL. Each line carries `channelSlug` and `slug`
+// ("<channel>/<id>") stamped at fetch time, and renameChannel moves the
+// directory without rewriting a line — nor would a re-fetch repair it: dedupe
+// is by id, so an unchanged post is never written again. Trusting the stamp
+// left every post of a renamed channel naming its OLD slug: the index keyed
+// the page by the new one while the record inside said the old, so postsCache
+// could not find a post on its own page, PostModal and threads threw, and MCP
+// links named a channel that no longer exists. Every reader comes through here
+// with `channels/<slug>` as the root, so both fields are re-derived from it.
export async function readPostShard(
channelRoot: string,
shard: string,
@@ -248,6 +259,7 @@ export async function readPostShard(
} catch {
return [];
}
+ const channelSlug = path.basename(channelRoot);
const posts: Post[] = [];
for (const line of text.split("\n")) {
const trimmed = line.trim();
@@ -259,7 +271,12 @@ export async function readPostShard(
continue;
}
const post = parsePost(raw);
- if (post) posts.push(post);
+ if (!post) continue;
+ if (post.channelSlug !== channelSlug) {
+ post.channelSlug = channelSlug;
+ post.slug = postSlug(channelSlug, post.id);
+ }
+ posts.push(post);
}
return posts;
}
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **A renamed social channel's posts open again.** Each archived post carries the channel slug it was fetched under, and renaming the channel moves its directory without rewriting them, so every post of a renamed channel named the old slug: the index filed it under the new one, the post page and its thread could not find it there, and MCP links named a channel that no longer existed. A post's channel and slug are now read from the directory it is stored in, wherever it was fetched; the files are not rewritten. The next index build corrects the published records. Needs a restart of the editor.
- **A video's other English tracks are readable and searchable where their words differ.** Uploaded captions are not always a transcript of what was said, so the tracks beside the transcript stay: the served `en` beside `en-orig`, a regional or auto-translated track, and the captions a local transcription replaced. One is kept where its words differ from the transcript's and from every track kept before it; identical tracks, most of them, add nothing. The index keeps them in an `alts` sub-DB and writes `track` and `altTracks` onto the transcript record only then, so every other record's page is what it was. A search hit in a word only an alternate holds names the track; one every track says is found once, in the transcript. The video page's **Transcript** card reads the transcript and switches tracks ("Track: original audio captions ▾"); switching changes nothing on disk, and **Set as transcript** stays the way the transcript itself changes. English VTTs are no longer shipped as subtitle tracks. One notion of a track — ids, plain labels, which are kept, how a hit across them is found — lives in `common/lib/captionTracks.ts`.
- **The next index build reads the alternate tracks once.** Every record that can hold one — two or more English VTTs, or a transcription beside captions — is re-read from disk, and nothing else; the log says `Alternate tracks v1: N record(s) re-read.` and how many hold a track whose words differ. The version is recorded only when no channel is held. A transcribed video's captions now count toward its change time, so a later caption fetch reaches the index.
- **The MCP reads every English track.** `search_transcripts` and the query-tree tools match a record's alternate tracks and tag a snippet from one (`[in uploaded captions 1:30]`); `get_transcript` names a video's tracks in its header and reads another with `track`; `get_transcripts` windows a match only an alternate holds, under its name; `get_video_metadata` lists the other tracks without their cues. The sweep plan says what such a hit is before it is quoted.