commit c5819828c5a08a52d18e419f10dae3dc4a100ec0
parent 06209852b27ec8008b311e4cf8a0e3fc34721d47
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 5 Oct 2026 05:22:37 -0400
hub and homepage: a cited report site is never listed (isListedSite) — no hub member, no homepage card, total or channel-sites entry, no footer link; a full site with reports is listed as before; SITE.md regenerated
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
9 files changed, 97 insertions(+), 10 deletions(-)
diff --git a/SITE.md b/SITE.md
@@ -163,7 +163,7 @@ Default: absent
## `listed`
-Whether the family lists this site. Opt-OUT: absent/true = listed, only an explicit `false` is written. An unlisted site still builds and deploys as before, and its own pages are unchanged; it is left out of the homepage (cards, chart, `/stats`), the hub (members, federated search, `/corpus.json`, `/llms.txt`), every other site's footer, and the published `channel-sites.json` and pooled `stats/`. A channel only unlisted sites expose is in none of the family's public totals; a channel a listed site also exposes is credited to the listed one.
+Whether the family lists this site. Opt-OUT: absent/true = listed, only an explicit `false` is written. An unlisted site still builds and deploys as before, and its own pages are unchanged; it is left out of the homepage (cards, chart, `/stats`), the hub (members, federated search, `/corpus.json`, `/llms.txt`), every other site's footer, and the published `channel-sites.json` and pooled `stats/`. A channel only unlisted sites expose is in none of the family's public totals; a channel a listed site also exposes is credited to the listed one. A private site and a cited site (`publish: "cited"`) are never listed, whatever `listed` says.
Default: `true`
@@ -175,7 +175,7 @@ Default: absent
## `publish`
-What this site publishes. `"full"` (the default; absent): the searchable corpus of its `channels`. `"cited"`: only the site's `reports` and the moments they cite — no search, no browse, no full transcripts, no archives; `channels` is then the pool its citations may resolve against. The cited scope is applied by the reports pipeline at build time. Only `"cited"` is written; any other value reads as `"full"`.
+What this site publishes. `"full"` (the default; absent): the searchable corpus of its `channels`. `"cited"`: only the site's `reports` and the moments they cite — no search, no browse, no full transcripts, no archives; `channels` is then the pool its citations may resolve against. The cited scope is applied by the reports pipeline at build time. A cited site is not a searchable archive, so the family does not list it (as `listed: false`, whatever `listed` says): no hub membership, no homepage card or totals, no footer link from other sites. Only `"cited"` is written; any other value reads as `"full"`.
Default: `"full"`
diff --git a/common/bin/compose-hub.test.ts b/common/bin/compose-hub.test.ts
@@ -172,6 +172,47 @@ test("an unlisted site is in none of the hub's files; a listed one is in each",
}
});
+// Report sites: a cited site (`publish: "cited"`) publishes reports, not a
+// searchable archive — no hub member, whatever `listed` says. A full site with
+// reports stays one.
+test("a cited site is no hub member; a full site with reports is", 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-full", {
+ siteTitle: "Full Fixture",
+ siteUrl: "https://fixture-full.example",
+ reports: ["demo-report"],
+ });
+ write("fixture-cited", {
+ siteTitle: "Cited Fixture",
+ siteUrl: "https://fixture-cited.example",
+ publish: "cited",
+ listed: true,
+ });
+ console.log = () => {};
+ 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-full"]);
+ for (const f of ["corpus.json", "llms.txt"]) {
+ const text = readFileSync(path.join(paths.exportPublicDir, f), "utf8");
+ assert.ok(text.includes("fixture-full.example"), `${f} lists the full site`);
+ assert.ok(!text.includes("fixture-cited"), `${f} names the cited site`);
+ }
+ } finally {
+ console.log = log;
+ 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
diff --git a/common/bin/compose-hub.ts b/common/bin/compose-hub.ts
@@ -5,7 +5,8 @@
// reads every archive cross-origin at runtime. It emits:
//
// public/hub-sites.json <- the built-in trusted pool (listSites with a siteUrl,
-// listed — site.json `listed`, isListedSite)
+// listed — site.json `listed`, isListedSite; never
+// a private or a cited report site)
// public/hub-summary.json <- the official instances' numbers, the homepage's
// own (lib/hubSummary.ts) — OPTIONAL: skipped when
// there is no index to walk
diff --git a/common/controller/poolSummary.test.ts b/common/controller/poolSummary.test.ts
@@ -27,6 +27,16 @@ test("channel-sites.json names listed sites only; a channel only an unlisted sit
assert.ok(!JSON.stringify(channelSitesOf(sites)).includes("fixture-unlisted"));
});
+// Report sites: a CITED site is never listed either — its channels are its
+// citations' pool, not a published archive.
+test("channel-sites.json leaves out a cited site", () => {
+ const sites = [
+ parseSite("fixture-a", { channels: [{ slug: "shared" }] }),
+ parseSite("fixture-cited", { publish: "cited", channels: [{ slug: "shared" }, { slug: "own" }] }),
+ ];
+ assert.deepEqual(channelSitesOf(sites), { shared: ["fixture-a"] });
+});
+
// Release 17 slice XP: a PRIVATE site is never listed, so it is in neither; and
// with X posts private an X channel a public site leaves out of its build is
// not mapped to that site (buildPoolSummary passes the narrowing).
diff --git a/common/lib/homepageSummary.test.ts b/common/lib/homepageSummary.test.ts
@@ -305,6 +305,17 @@ test("an unlisted site is in no array and no total; a channel it shares is the l
assert.equal(s.version, 6);
});
+// Report sites: a cited site (`publish: "cited"`) is not a searchable archive,
+// so the homepage lists it no more than an unlisted one.
+test("a cited site is in no array and no total, exactly as an unlisted one", () => {
+ const cited = { ...site("zeta", ["q1", "a1"], "https://zeta.example"), publish: "cited" } as Site;
+ const channelSites = { ...CHANNEL_SITES, a1: ["alpha", "zeta"], q1: ["zeta"] };
+ const own = [stat({ channelSlug: "q1", id: "q-1", uploadDate: "20251101", duration: 7200 })];
+ const s = buildHomepageSummary([...STATS, ...own], channelSites, [...SITES, cited], NOW);
+ assert.deepEqual(s, buildHomepageSummary(STATS, CHANNEL_SITES, SITES, NOW));
+ assert.ok(!JSON.stringify(s).includes("zeta"));
+});
+
test("an unlisted site with no siteUrl, or with every channel shared, changes nothing either", () => {
const base = buildHomepageSummary(STATS, CHANNEL_SITES, SITES, NOW);
// No siteUrl: never public, and its own channel is still in no total (unlike
diff --git a/common/lib/homepageSummary.ts b/common/lib/homepageSummary.ts
@@ -12,7 +12,8 @@ import { VIDEO_STATES, type VideoState } from "./availability";
// multi-MB whole-pool stats dataset.
//
// Scope: the chart "universe" is PUBLIC sites only (those with a siteUrl that
-// are listed — site.json `listed`, lib/siteSchema.ts isListedSite), and every
+// are listed — site.json `listed`, lib/siteSchema.ts isListedSite; never a
+// private or a cited report site), and every
// video is attributed to a single PRIMARY public site (the first, by sorted id,
// exposing its channel) so the Site and Channel breakdowns partition the same
// set and combined totals stay honest. The KPI `totals` (and `availability`)
@@ -43,6 +44,10 @@ import { VIDEO_STATES, type VideoState } from "./availability";
// and a channel only unlisted sites expose is in no total — `totals` and
// `availability` included. No field was added or removed; the number says the
// totals' scope moved.
+//
+// Still v6: a cited report site (site.json `publish: "cited"`) is left out
+// exactly as an unlisted one (lib/siteSchema.ts isListedSite) — the same scope
+// rule, applied to a kind of site no summary had yet counted.
export const HOMEPAGE_SUMMARY_VERSION = 6;
// Day buckets are capped to this many trailing days so the embedded summary stays
diff --git a/common/lib/siteSchema.test.ts b/common/lib/siteSchema.test.ts
@@ -376,6 +376,19 @@ test("isListedSite is the key's default; channelsOnlyOnUnlistedSites keeps a sha
assert.deepEqual([...channelsOnlyOnUnlistedSites([sites[0]])], []);
});
+test("a cited site is never listed, whatever `listed` says; a full site with reports is listed as before", () => {
+ assert.equal(isListedSite({ publish: "cited" }), false);
+ assert.equal(isListedSite({ publish: "cited", listed: true }), false);
+ assert.equal(isListedSite({ publish: "full" }), true);
+ assert.equal(isListedSite(parseSite("full-with-reports", { reports: ["demo-report"] })), true);
+ const sites = [
+ parseSite("shown", { channels: [{ slug: "shared" }] }),
+ parseSite("reports", { publish: "cited", channels: [{ slug: "shared" }, { slug: "pool-only" }] }),
+ ];
+ // A cited site's channels are its citations' pool, not a published archive.
+ assert.deepEqual([...channelsOnlyOnUnlistedSites(sites)], ["pool-only"]);
+});
+
test("the footer never links an unlisted sibling, and an unlisted site's own footer still lists the rest", () => {
const current = parseSite("cur", {
siteUrl: "https://cur.example",
diff --git a/common/lib/siteSchema.ts b/common/lib/siteSchema.ts
@@ -184,11 +184,11 @@ export const SITE_FIELD_DOCS: FieldDocs<Site> = {
siteUrl:
"Absolute public URL of this site's deployment, e.g. `https://jeralyzer.pages.dev` (trimmed, trailing slashes removed; anything not absolute http(s) is dropped). Drives the cross-site footer: a site with no siteUrl is omitted from every other site's list.",
listed:
- "Whether the family lists this site. Opt-OUT: absent/true = listed, only an explicit `false` is written. An unlisted site still builds and deploys as before, and its own pages are unchanged; it is left out of the homepage (cards, chart, `/stats`), the hub (members, federated search, `/corpus.json`, `/llms.txt`), every other site's footer, and the published `channel-sites.json` and pooled `stats/`. A channel only unlisted sites expose is in none of the family's public totals; a channel a listed site also exposes is credited to the listed one.",
+ "Whether the family lists this site. Opt-OUT: absent/true = listed, only an explicit `false` is written. An unlisted site still builds and deploys as before, and its own pages are unchanged; it is left out of the homepage (cards, chart, `/stats`), the hub (members, federated search, `/corpus.json`, `/llms.txt`), every other site's footer, and the published `channel-sites.json` and pooled `stats/`. A channel only unlisted sites expose is in none of the family's public totals; a channel a listed site also exposes is credited to the listed one. A private site and a cited site (`publish: \"cited\"`) are never listed, whatever `listed` says.",
audience:
'Who this site is built for. `"public"` (the default; absent) or `"private"`: the operator\'s own reading copy, built on this machine and never deployed — every deploy path (Build & deploy, Deploy, `archilyzer deploy site`, Build & deploy all, docker/publish-site.sh) refuses it before any upload, while a build without a deploy still works. A private site is never listed (as `listed: false`, whatever `listed` says), publishes no `hubUrl`, and its `/corpus.json` says `"audience": "private"`. Content kept from the public — X posts while `social.x.visibility` is `"private"` — is built only into private sites. Only `"private"` is written.',
publish:
- 'What this site publishes. `"full"` (the default; absent): the searchable corpus of its `channels`. `"cited"`: only the site\'s `reports` and the moments they cite — no search, no browse, no full transcripts, no archives; `channels` is then the pool its citations may resolve against. The cited scope is applied by the reports pipeline at build time. Only `"cited"` is written; any other value reads as `"full"`.',
+ 'What this site publishes. `"full"` (the default; absent): the searchable corpus of its `channels`. `"cited"`: only the site\'s `reports` and the moments they cite — no search, no browse, no full transcripts, no archives; `channels` is then the pool its citations may resolve against. The cited scope is applied by the reports pipeline at build time. A cited site is not a searchable archive, so the family does not list it (as `listed: false`, whatever `listed` says): no hub membership, no homepage card or totals, no footer link from other sites. Only `"cited"` is written; any other value reads as `"full"`.',
reports:
"The site's published reports, in display order: report ids (lowercase slugs, `[a-z0-9][a-z0-9-]*`), each a directory under `sites/<siteId>/reports/`. A report directory not named here is a draft and is not published. Invalid and repeated ids are dropped. Absent/empty = no reports.",
relatedSites:
@@ -224,9 +224,13 @@ export function isValidSiteId(id: unknown): id is string {
// pure summary builder can use it without importing file I/O.
//
// A PRIVATE site (`audience: "private"`) is never listed, whatever `listed`
-// says: it is never deployed, so there is nothing at its URL to list.
-export function isListedSite(site: Pick<Site, "listed" | "audience">): boolean {
- return site.listed !== false && !isPrivateSite(site);
+// says: it is never deployed, so there is nothing at its URL to list. Nor is a
+// CITED site (`publish: "cited"`): it publishes reports and the moments they
+// cite, not a searchable archive, so it is no hub member (federated search
+// would find no channel there), no homepage card and in no family total. A
+// full site with reports is listed as before.
+export function isListedSite(site: Pick<Site, "listed" | "audience" | "publish">): boolean {
+ return site.listed !== false && !isPrivateSite(site) && !isCitedSite(site);
}
// The channels whose content belongs to unlisted sites alone: exposed by at
@@ -235,7 +239,7 @@ export function isListedSite(site: Pick<Site, "listed" | "audience">): boolean {
// and a channel no site exposes (pool-only) is not here either — the family's
// instance-wide totals have always counted it.
export function channelsOnlyOnUnlistedSites(
- sites: readonly Pick<Site, "listed" | "audience" | "channels">[],
+ sites: readonly Pick<Site, "listed" | "audience" | "publish" | "channels">[],
): Set<string> {
const onListed = new Set<string>();
const onUnlisted = new Set<string>();
diff --git a/editor/app/sites/components/SiteForm.tsx b/editor/app/sites/components/SiteForm.tsx
@@ -377,6 +377,8 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) {
A cited-only site publishes no search, browse, full transcripts or
archives; its channels are the pool its reports' citations resolve
against. The reports pipeline applies this scope when the site is built.
+ It is never listed on the homepage or the hub: it is not a searchable
+ archive.
</span>
</label>
<div className="flex flex-col gap-1 text-sm">