Archilyzer · Source

archilyzer

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

commit 11880bf4ad5f484ce4e65544b277b5e05652a5f1
parent 3a349be6fcdc1b9c45146943cf75601a3af33814
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 21:05:27 -0400

common: the hub carries no site's data — compose-hub clears every per-site entry from public/ and ships the global aliases; builtHubProblem refuses a hub bundle that still carries a site's data trees (review HIGH 1)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mcommon/bin/compose-hub.test.ts | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/bin/compose-hub.ts | 40++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/builtExport.test.ts | 8++++++++
Mcommon/lib/builtExport.ts | 14++++++++++++++
4 files changed, 114 insertions(+), 0 deletions(-)

diff --git a/common/bin/compose-hub.test.ts b/common/bin/compose-hub.test.ts @@ -16,6 +16,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { getPaths, type Paths } from "../lib/paths"; import { main } from "./compose-hub"; +import { readGlobalAliases } from "../lib/aliasesStore"; // Run with: // pnpm --filter yt-dlp-transcript-common test @@ -170,3 +171,54 @@ test("an unlisted site is in none of the hub's files; a listed one is in each", rmSync(root, { recursive: true, force: true }); } }); + +// Release 17 slice XP (the review's HIGH 1): a site's compose leaves its data +// in public/ — a private site's X posts included — and the hub builds from +// public/ next. compose-hub removes every per-site entry, through a link only +// the link, and ships the global alias dictionary as its own. +test("compose-hub removes a site's data from public/, a linked entry by its link only", async () => { + const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); + const log = console.log; + try { + const paths = fixturePaths(root); + const pub = paths.exportPublicDir; + // What a private site's compose leaves. + for (const tree of ["summaries", "transcripts", "subs", "digests", "stats", "archives"]) { + mkdirSync(path.join(pub, tree, "x"), { recursive: true }); + writeFileSync(path.join(pub, tree, "x", "page-0000.json"), "[]"); + } + for (const f of ["site.json", "tags.json", "duplicates.json", "chart-templates.json", "sitemap.xml"]) { + writeFileSync(path.join(pub, f), "{}"); + } + writeFileSync(path.join(pub, "search-aliases.json"), JSON.stringify({ aliases: [{ site: 1 }] })); + // posts/ as a worktree has it: a link into the primary checkout. + const primaryPosts = path.join(root, "primary-public", "posts"); + mkdirSync(path.join(primaryPosts, "jer-x"), { recursive: true }); + writeFileSync(path.join(primaryPosts, "manifest.json"), '{"channels":[{"slug":"jer-x"}]}'); + symlinkSync(primaryPosts, path.join(pub, "posts")); + // A checked-in static asset stays. + writeFileSync(path.join(pub, "globe.svg"), "<svg/>"); + // No global dictionary file: the seeded defaults are the global one. + const hubPaths = { ...paths, globalAliasesFile: path.join(root, "no-aliases.json") }; + + console.log = () => {}; + await main({ paths: hubPaths }); + console.log = log; + + for (const gone of [ + "summaries", "transcripts", "subs", "posts", "digests", "stats", "archives", + "site.json", "tags.json", "duplicates.json", "chart-templates.json", "sitemap.xml", + ]) { + assert.ok(!existsSync(path.join(pub, gone)), `${gone} was removed`); + } + assert.ok(existsSync(path.join(primaryPosts, "manifest.json")), "the link's target is untouched"); + assert.ok(existsSync(path.join(pub, "globe.svg"))); + assert.ok(existsSync(path.join(pub, "hub-sites.json"))); + const aliases = JSON.parse(readFileSync(path.join(pub, "search-aliases.json"), "utf8")); + assert.deepEqual(aliases, { aliases: readGlobalAliases(hubPaths).aliases }); + assert.ok(!JSON.stringify(aliases).includes('"site"'), "not the site's aliases"); + } finally { + console.log = log; + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/common/bin/compose-hub.ts b/common/bin/compose-hub.ts @@ -11,6 +11,10 @@ // there is no index to walk // public/_headers <- CORS for the hub's own served JSON // public/sw.js <- the hub service worker (the hub always ships a PWA) +// public/search-aliases.json <- the global alias dictionary +// +// and REMOVES every per-site entry a site's compose left in public/ +// (SITE_ONLY_PUBLIC_ENTRIES below): the hub holds no site's data. // // The hub's branding ("Archilyzer") is resolved at build/render time from the // HomepageConfig (see export/app/lib/site.ts hubSite()), not composed here. @@ -32,6 +36,7 @@ import { import { HUB_CORS_PATHS, renderHeadersFile } from "../lib/archive/headers"; import { buildPoolSummary } from "../controller/poolSummary"; import { HUB_SUMMARY_FILE, toHubSummary } from "../lib/hubSummary"; +import { readGlobalAliases } from "../lib/aliasesStore"; import { runIfEntryPoint } from "./_cli"; import { writePublicFile } from "./_publicFile"; @@ -76,10 +81,45 @@ async function composeHubSummary( } } +// THE HUB CARRIES NO SITE'S DATA (release 17 slice XP, the review's HIGH 1). +// public/ is the one directory every site composes into in turn, and the hub +// builds from it next: whatever the last site's compose left there — its data +// trees and its per-site files — `next build` copied into the hub's out/, and +// the hub deploy shipped it. The live hub served jeralyzer's posts manifest; +// after a PRIVATE site's build it would have served every X post. So the hub's +// compose removes every per-site entry first. A worktree's public/ entries are +// links into the primary checkout: rm removes the link, never its target. +export const SITE_ONLY_PUBLIC_ENTRIES: readonly string[] = [ + "summaries", + "transcripts", + "subs", + "posts", + "digests", + "stats", + "archives", + "site.json", + "tags.json", + "duplicates.json", + "search-aliases.json", + "chart-templates.json", + "sitemap.xml", +]; + export async function main(opts: { paths?: Paths } = {}): Promise<void> { const paths = opts.paths ?? getPaths(); const publicDir = paths.exportPublicDir; + for (const entry of SITE_ONLY_PUBLIC_ENTRIES) { + await rm(path.join(publicDir, entry), { recursive: true, force: true }); + } + // The hub's own alias dictionary is the global one (no site's overrides): + // what a hub reader loads for hub-wide search (lib/archive/reader-hub.ts), + // where it used to get whichever site had composed last. + await writePublicFile( + path.join(publicDir, "search-aliases.json"), + JSON.stringify({ aliases: readGlobalAliases(paths).aliases }), + ); + // Built-in pool: every configured site that publishes a public URL and is // listed. An unlisted site (`listed: false`) still builds and deploys, but the // hub does not list it: not a member, not in federated search, not in the diff --git a/common/lib/builtExport.test.ts b/common/lib/builtExport.test.ts @@ -116,6 +116,14 @@ test("builtHubProblem accepts only a hub bundle", () => { builtHubProblem(none.dir), "export/out holds no hub build — build the hub first", ); + + // Release 17 XP: a hub bundle composed over a site's data is refused. + mkdirSync(path.join(hub.dir, "posts", "jer-x"), { recursive: true }); + mkdirSync(path.join(hub.dir, "summaries")); + assert.equal( + builtHubProblem(hub.dir), + "export/out holds a hub build that still carries a site's data (summaries, posts) — build the hub again", + ); } finally { hub.cleanup(); site.cleanup(); diff --git a/common/lib/builtExport.ts b/common/lib/builtExport.ts @@ -185,9 +185,23 @@ export function builtHubProblem(outDir: string): string | null { if (!existsSync(path.join(outDir, "hub-sites.json"))) { return "export/out holds no hub build — build the hub first"; } + // The hub holds no site's data (compose-hub removes it): a hub bundle that + // still carries a site's data trees was composed over one, and could ship + // that site's posts — a private site's included. + const carried = HUB_FORBIDDEN_TREES.filter((tree) => existsSync(path.join(outDir, tree))); + if (carried.length > 0) { + return ( + `export/out holds a hub build that still carries a site's data (${carried.join(", ")}) — ` + + `build the hub again` + ); + } return null; } +// The per-site data trees a hub bundle must never carry (the trees of +// compose-hub's SITE_ONLY_PUBLIC_ENTRIES). +const HUB_FORBIDDEN_TREES = ["summaries", "transcripts", "subs", "posts", "digests", "stats", "archives"]; + /** * Why `outDir` — the homepage package's `homepage/out` — may not be deployed as * the homepage, as one sentence, or null when it holds a build.