commit ef987279bed05be673c522ab14a6b222740ba2fa
parent 286c422999e8d65b57187aa19d6f6b02d6c2c7f2
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 29 Sep 2026 20:46:18 -0400
common: the hub does not list an unlisted site — not in hub-sites.json, so not a member, not in federated search, the hub's corpus.json or llms.txt
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 48 insertions(+), 4 deletions(-)
diff --git a/common/bin/compose-hub.test.ts b/common/bin/compose-hub.test.ts
@@ -130,3 +130,43 @@ test("in a worktree, compose-hub writes its own files and never through the link
rmSync(root, { recursive: true, force: true });
}
});
+
+// Release 14 slice HS: an unlisted site builds and deploys, and the hub does
+// not list it — not a member, so not in federated search, corpus.json or
+// llms.txt.
+test("an unlisted site is in none of the hub's files; a listed one is in each", async () => {
+ const root = mkdtempSync(path.join(tmpdir(), "compose-hub-"));
+ const log = console.log;
+ try {
+ const paths = fixturePaths(root);
+ const write = (id: string, site: Record<string, unknown>) => {
+ mkdirSync(path.join(paths.sitesDir, id), { recursive: true });
+ writeFileSync(path.join(paths.sitesDir, id, "site.json"), JSON.stringify(site));
+ };
+ write("fixture-shown", { siteTitle: "Shown Fixture", siteUrl: "https://fixture-shown.example" });
+ write("fixture-unlisted", {
+ siteTitle: "Unlisted Fixture",
+ siteUrl: "https://fixture-unlisted.example",
+ listed: false,
+ });
+ const lines: string[] = [];
+ console.log = (...a: unknown[]) => void lines.push(a.join(" "));
+ await main({ paths });
+ console.log = log;
+ const pool = JSON.parse(
+ readFileSync(path.join(paths.exportPublicDir, "hub-sites.json"), "utf8"),
+ ) as Array<{ siteId: string }>;
+ assert.deepEqual(pool.map((s) => s.siteId), ["fixture-shown"]);
+ for (const f of ["hub-sites.json", "corpus.json", "llms.txt"]) {
+ const text = readFileSync(path.join(paths.exportPublicDir, f), "utf8");
+ assert.ok(text.includes("fixture-shown.example"), `${f} lists the listed site`);
+ for (const needle of ["fixture-unlisted", "Unlisted Fixture"]) {
+ assert.ok(!text.includes(needle), `${f} names ${needle}`);
+ }
+ }
+ assert.match(lines.join("\n"), /compose-hub: 1 built-in pool site\(s\)/);
+ } finally {
+ console.log = log;
+ rmSync(root, { recursive: true, force: true });
+ }
+});
diff --git a/common/bin/compose-hub.ts b/common/bin/compose-hub.ts
@@ -4,7 +4,8 @@
// there is no SITE_ID and no per-site data: the hub is a federating shell that
// reads every archive cross-origin at runtime. It emits:
//
-// public/hub-sites.json <- the built-in trusted pool (listSites with a siteUrl)
+// public/hub-sites.json <- the built-in trusted pool (listSites with a siteUrl,
+// listed — site.json `listed`, isListedSite)
// public/hub-summary.json <- the official instances' numbers, the homepage's
// own (lib/hubSummary.ts) — OPTIONAL: skipped when
// there is no index to walk
@@ -19,7 +20,7 @@ import { existsSync } from "node:fs";
import { cp, rm, access } from "node:fs/promises";
import { getPaths, type Paths } from "../lib/paths";
import { accentHex } from "../lib/accent";
-import { listSites, resolveHubUrl } from "../lib/site";
+import { isListedSite, listSites, resolveHubUrl } from "../lib/site";
import { getHomepageConfig } from "../lib/homepage";
import { SITE_DESCRIPTOR_VERSION } from "../lib/siteDescriptor";
import {
@@ -79,7 +80,10 @@ export async function main(opts: { paths?: Paths } = {}): Promise<void> {
const paths = opts.paths ?? getPaths();
const publicDir = paths.exportPublicDir;
- // Built-in pool: every configured site that publishes a public URL. The entry
+ // 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
+ // hub's corpus.json or llms.txt (both are built from this list). The entry
// the hub registry loads at boot to seed its trusted built-in pool, and the
// input buildHubCorpus maps — ONE type for both (lib/archive/contract.ts
// HubMemberInput), where this file used to restate it as a local
@@ -88,7 +92,7 @@ export async function main(opts: { paths?: Paths } = {}): Promise<void> {
// a siteUrl can't be federated and is dropped — by both consumers.
const builtins: HubMemberInput[] = [];
for (const site of listSites(paths)) {
- if (!site.siteUrl) continue;
+ if (!site.siteUrl || !isListedSite(site)) continue;
builtins.push({
siteId: site.siteId,
siteTitle: site.siteTitle,