Archilyzer · Source

archilyzer

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

commit 7fff691b64dc662dba14d140114f6b30747518dd
parent 9d38f625e5cf7b01a99cdba857b74aa52927477e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 02:11:06 -0400

common: site.json `publish` ("full" | "cited") and `reports` (ordered report ids); SITE.md regenerated

- publish: absent reads "full"; only "cited" is written. isCitedSite is the
  one predicate. The cited scope is applied by the reports pipeline; until it
  is, a cited site builds as a full one.
- reports: ordered report ids, each a slug ([a-z0-9][a-z0-9-]*); invalid and
  repeated ids are dropped (parseSiteReports); written only when non-empty.
- Tests: defaults read and are not written, cited round-trips through
  writeSite/getSite, invalid report ids dropped on read and on write.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
MSITE.md | 18++++++++++++++++++
Mcommon/lib/siteSchema.test.ts | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/siteSchema.ts | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 128 insertions(+), 0 deletions(-)

diff --git a/SITE.md b/SITE.md @@ -25,6 +25,8 @@ Regenerate this file with `pnpm --filter yt-dlp-transcript-common exec tsx bin/f | [`siteUrl`](#siteurl) | absent | | [`listed`](#listed) | `true` | | [`audience`](#audience) | absent | +| [`publish`](#publish) | `"full"` | +| [`reports`](#reports) | `[]` | | [`relatedSites`](#relatedsites) | `[]` | | [`pwa`](#pwa) | `false` | | [`archives`](#archives) | `true` | @@ -171,6 +173,22 @@ Who this site is built for. `"public"` (the default; absent) or `"private"`: the 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"`. + +Default: `"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. + +Default: + +```json +[] +``` + ## `relatedSites` Pulls specific siblings to the front of the footer's cross-site list, in named groups. Siblings not named here fall into a trailing "Other sites" group. Absent/empty = one flat list of every sibling. diff --git a/common/lib/siteSchema.test.ts b/common/lib/siteSchema.test.ts @@ -10,8 +10,10 @@ import { SITE_FIELD_DOCS, SITE_KEYS, channelsOnlyOnUnlistedSites, + isCitedSite, isListedSite, parseSite, + parseSiteReports, siteFieldsSchema, siteToDisk, type Site, @@ -64,6 +66,8 @@ test("empty, null, [] and a number all read as the defaults, every key emitted", assert.equal(want.pwa, false); assert.equal(want.listed, true); assert.deepEqual(want.relatedSites, []); + assert.equal(want.publish, "full"); + assert.deepEqual(want.reports, []); assert.equal(want.socialLinks, undefined); assert.ok("socialLinks" in want); for (const raw of [null, [], 3, "x", undefined]) { @@ -235,6 +239,8 @@ function fixtures(): Array<[string, unknown]> { cloudflareProject: "p", siteUrl: "https://s.example//", listed: false, + publish: "cited", + reports: ["r-one", "r-one", "Bad", "", 7, "r-two"], relatedSites: [{ siteIds: ["x", "x", "BAD"] }, { label: " ", siteIds: [] }], pwa: true, archives: false, @@ -310,6 +316,53 @@ test("listed: absent reads listed, only an explicit false unlists, and only fals assert.equal(getSite("s", paths).listed, true); }); +test("publish: absent reads full, only cited is kept, and only cited is written", () => { + for (const v of [undefined, "full", "FULL", "Cited", "corpus", true, 1, null]) { + const site = parseSite("s", v === undefined ? {} : { publish: v }); + assert.equal(site.publish, "full", String(v)); + assert.equal(isCitedSite(site), false, String(v)); + assert.equal("publish" in siteToDisk(site), false, String(v)); + } + const cited = parseSite("s", { publish: "cited" }); + assert.equal(cited.publish, "cited"); + assert.equal(isCitedSite(cited), true); + assert.equal(siteToDisk(cited).publish, "cited"); + assert.equal(isCitedSite({}), false); +}); + +test("reports: ordered slugs, invalid and repeated ids dropped, written only when non-empty", () => { + assert.deepEqual( + parseSiteReports(["b-two", "a-one", "b-two", "Upper", "-lead", "has space", "", 3, null, "c3"]), + ["b-two", "a-one", "c3"], + ); + for (const v of [undefined, null, "a-one", { a: 1 }, 5]) { + assert.deepEqual(parseSite("s", v === undefined ? {} : { reports: v }).reports, [], String(v)); + } + const site = parseSite("s", { reports: ["z-last", "a-first", "a-first"] }); + assert.deepEqual(site.reports, ["z-last", "a-first"]); + assert.deepEqual(siteToDisk(site).reports, ["z-last", "a-first"]); + assert.equal("reports" in siteToDisk(parseSite("s", { reports: [] })), false); + assert.equal("reports" in siteToDisk(parseSite("s", { reports: ["BAD"] })), false); + // siteToDisk re-parses a caller's list: a bad id handed to the writer is not persisted. + assert.deepEqual(siteToDisk({ ...site, reports: ["ok", "NOPE", "ok"] }).reports, ["ok"]); +}); + +test("writeSite → getSite round-trips a cited site and its reports; a full site writes neither key", async () => { + const paths = scratchPaths(await mkdtemp(path.join(os.tmpdir(), "site-"))); + const cited = parseSite("s", { publish: "cited", reports: ["first", "second"] }); + await writeSite(cited, paths); + assert.deepEqual(getSite("s", paths), cited); + const disk = JSON.parse(await readFile(siteConfigFile(paths, "s"), "utf8")); + assert.equal(disk.publish, "cited"); + assert.deepEqual(disk.reports, ["first", "second"]); + await writeSite({ ...cited, publish: "full", reports: [] }, paths); + const plain = JSON.parse(await readFile(siteConfigFile(paths, "s"), "utf8")); + assert.equal("publish" in plain, false); + assert.equal("reports" in plain, false); + assert.equal(getSite("s", paths).publish, "full"); + assert.deepEqual(getSite("s", paths).reports, []); +}); + test("isListedSite is the key's default; channelsOnlyOnUnlistedSites keeps a shared channel with the listed site", () => { assert.equal(isListedSite({}), true); assert.equal(isListedSite({ listed: true }), true); @@ -353,4 +406,6 @@ test("writeSite → getSite round-trips, and the file holds only non-defaults", assert.equal("transcriptDownloads" in disk, false); assert.equal("pwa" in disk, false); assert.equal("listed" in disk, false); + assert.equal("publish" in disk, false); + assert.equal("reports" in disk, false); }); diff --git a/common/lib/siteSchema.ts b/common/lib/siteSchema.ts @@ -89,6 +89,46 @@ export function isPrivateSite(site: Pick<Site, "audience">): boolean { return site.audience === "private"; } +// What a site publishes (report sites). "full" (the default, never written) is +// the searchable corpus every site has always been. "cited" publishes only the +// site's reports and the moments they cite; the reports pipeline applies that +// scope, and until it does a cited site builds as a full one. +export type SitePublish = "full" | "cited"; + +export const SITE_PUBLISH_SCOPES: readonly SitePublish[] = ["full", "cited"]; + +export function isSitePublish(v: unknown): v is SitePublish { + return v === "full" || v === "cited"; +} + +// THE ONE PREDICATE for a cited-only site. Absent or anything but "cited" reads +// as full. +export function isCitedSite(site: Pick<Site, "publish">): boolean { + return site.publish === "cited"; +} + +// A report id: a lowercase slug, the directory name under +// `sites/<siteId>/reports/`. The same grammar as a site id. +export const REPORT_ID_RE = /^[a-z0-9][a-z0-9-]*$/; + +export function isValidReportId(id: unknown): id is string { + return typeof id === "string" && REPORT_ID_RE.test(id); +} + +// Parse the published report list: valid ids, first occurrence kept, order +// preserved. Anything else (a non-array, a bad or repeated id) is dropped. +export function parseSiteReports(input: unknown): string[] { + if (!Array.isArray(input)) return []; + const out: string[] = []; + const seen = new Set<string>(); + for (const id of input) { + if (!isValidReportId(id) || seen.has(id)) continue; + seen.add(id); + out.push(id); + } + return out; +} + // A Site is a selection + presentation layer over the single global channel // pool. Each field is documented in SITE_FIELD_DOCS below. export type Site = { @@ -107,6 +147,8 @@ export type Site = { siteUrl?: string; listed?: boolean; audience?: SiteAudience; + publish?: SitePublish; + reports?: string[]; relatedSites?: RelatedSiteGroup[]; pwa?: boolean; archives?: boolean; @@ -143,6 +185,10 @@ export const SITE_FIELD_DOCS: FieldDocs<Site> = { "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.", 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"`.', + 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: "Pulls specific siblings to the front of the footer's cross-site list, in named groups. Siblings not named here fall into a trailing \"Other sites\" group. Absent/empty = one flat list of every sibling.", pwa: @@ -312,6 +358,11 @@ export const siteFieldsSchema = z.object({ audience: settingsField((v): SiteAudience | undefined => v === "private" ? "private" : undefined, ).describe(d.audience), + // Only "cited" is kept; absent (and anything else) is the full default. + publish: settingsField((v): SitePublish => + v === "cited" ? "cited" : "full", + ).describe(d.publish), + reports: settingsField(parseSiteReports).describe(d.reports), relatedSites: settingsField(parseRelatedSites).describe(d.relatedSites), pwa: settingsField((v): boolean => v === true).describe(d.pwa), // Opt-out: only an explicit false disables. Absent/true stays on. @@ -378,6 +429,7 @@ export function siteToDisk(site: Site): Site { (c) => !c.groupId || groups.some((g) => g.id === c.groupId), ); const relatedSites = parseRelatedSites(site.relatedSites); + const reports = parseSiteReports(site.reports); const accent = parseAccentSetting(site.accent); const wordmarkLead = wordmarkLeadFor(site.headerTitle, site.wordmarkLead); const siteUrl = parseSiteUrl(site.siteUrl); @@ -405,6 +457,9 @@ export function siteToDisk(site: Site): Site { ...(site.listed === false ? { listed: false } : {}), // Public is the default: only the private audience is persisted. ...(isPrivateSite(site) ? { audience: "private" as const } : {}), + // Full is the default: only the cited scope is persisted. + ...(isCitedSite(site) ? { publish: "cited" as const } : {}), + ...(reports.length > 0 ? { reports } : {}), ...(relatedSites.length > 0 ? { relatedSites } : {}), ...(site.pwa ? { pwa: true } : {}), // Persist only the non-default: archives is on unless explicitly disabled.