Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mcommon/lib/posts-server.test.ts | 20++++++++++++++++++++
Mcommon/lib/posts-server.ts | 19++++++++++++++++++-
Meditor/CHANGELOG.md | 1+
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.