// WHICH SITES A CHANNEL'S POSTS ARE BUILT INTO (release 17 slice XP). // // THE ONE RULE, asked by both places a site's posts are decided: // - the index build's per-site loop (controller/buildIndex.ts), which writes // each site's summaries, subs, posts and digests manifests from the site's // member channels; // - the site compose (bin/compose-site.ts), which copies the shared // per-channel trees (transcripts, subs, posts, digests) of those members // into the served public dir and writes /site.json and /corpus.json. // Both narrow the site's members through `publishedMemberSlugs`, so the // manifests and the trees can never disagree about a channel. // // The rule: with `social.x.visibility` "private", an X channel (a social // channel on platform "twitter") is built only into a PRIVATE site // (`site.json` `audience: "private"`). Posts are all a social channel holds — it // has no videos (buildIndex's scan skips it) — so a public site leaves the // channel out whole: no posts tree, no posts-manifest entry, no empty channel // in its channel list or its corpus.json. Every other channel, and every // channel on a private site, is unaffected. Nothing on disk changes and // fetching does not: the shared posts tree (exportSharedPostsDir) and the LMDB // posts sub-DB are corpus-wide and keep every channel, which is what a private // site and the MCP over a private build read. // // The Search in row's Posts toggle needs no rule of its own: it shows only // when the site's posts manifest lists a channel (SearchSessionContext // hasPostsCorpus), so a public site whose only posts were X posts ships an // empty posts manifest and no Posts toggle. // // Pure: no fs, no settings read. The callers pass the settings and the configs. import { isSocialChannel, type ChannelConfig } from "./channelConfig"; import { isPrivateSite, type Site } from "./siteSchema"; import type { XPostsVisibility } from "../social/xCookieSource"; // An X channel: the posts of a social channel whose platform is X. export function isXPostsChannel( config: Pick | null | undefined, ): boolean { return isSocialChannel(config) && config?.platform === "twitter"; } // `social.x.visibility`, resolved: absent (or anything unknown) is "public". export function xPostsVisibility(settings: { social?: { x?: { visibility?: unknown } }; }): XPostsVisibility { return settings.social?.x?.visibility === "private" ? "private" : "public"; } // Whether `site` may carry the posts of the channel `config` describes. A // channel with no readable config is not an X channel as far as this rule // knows, and is left to whatever already decides its fate. export function postsVisibleTo( site: Pick, config: Pick | null | undefined, settings: { social?: { x?: { visibility?: unknown } } }, ): boolean { if (!isXPostsChannel(config)) return true; if (xPostsVisibility(settings) === "public") return true; return isPrivateSite(site); } // The site's member slugs, in membership order, less the channels whose posts // it may not carry (an X channel holds nothing else). `configOf` answers a // slug's channel config, or null/undefined when it has none. export function publishedMemberSlugs( site: Pick, configOf: ( slug: string, ) => Pick | null | undefined, settings: { social?: { x?: { visibility?: unknown } } }, ): string[] { return site.channels .map((c) => c.slug) .filter((slug) => postsVisibleTo(site, configOf(slug), settings)); }