Archilyzer · Source

archilyzer

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

commit 8fd36c5c246f56dff446f25bd2879ccc72c23fb2
parent 6430f16f59bbf65e3f23ddcb493bdd9e92e27d32
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 12 Sep 2026 01:45:39 -0400

plans: one-core Phase 2 sliced, and the umbrella corrected to match

The slice plan for Phase 2 (one ArchiveReader) with the three corrections
the 2026-09-12 survey found: eight component caches share the walk, not
four; the reader interface is mcp's 18-member ShardSource verbatim, with no
per-record fetch; config.dataDir belongs to umtool's cues.mjs, not to the
node reader. S1 is the only prerequisite; S2a, S2b, S2c, S3 and the legacy
pause-field deletion run in parallel off its merged tip. cues.mjs does not
adopt tsx in this phase (deferred to Phase 5, change list recorded in S2b).

plans/tools/jeralyzer-corpus-2026-09-12.json is the live /corpus.json taken
before S1 — the verification diffs a rebuilt site against it after S3.

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

Diffstat:
Aplans/one-core-phase-2.md | 240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplans/one-core.md | 58++++++++++++++++++++++++++++++++++++----------------------
Aplans/tools/jeralyzer-corpus-2026-09-12.json | 2++
3 files changed, 278 insertions(+), 22 deletions(-)

diff --git a/plans/one-core-phase-2.md b/plans/one-core-phase-2.md @@ -0,0 +1,240 @@ +# One core — Phase 2: the contract, one `ArchiveReader` + +## Context + +`plans/one-core.md` §Phase 2 is the umbrella. This is the slice-level plan: every walk of +the published archive's shard scheme — mcp's private reader, eight component caches, the +viewer's offline cache, umtool's cue resolver — collapses onto one `ArchiveReader` under +`common/lib/archive/`, and the two search pipelines become one `common/lib/search/`. + +It starts from `main` after the 2026-09 integration (`storage/relocate-media` + +`channel-priority/s5` merged on `integrate/2026-09-storage-priority` and fast-forwarded; +see `STATE.md`). Decisions taken with the operator on 2026-09-12: + +- Integration and rollout land first; Phase 2 slices branch off the merged `main`. +- umtool's `cues.mjs` does **not** adopt `tsx` in this phase. The umbrella said it would; + that is deferred to Phase 5 (projects join the core) and the change list is recorded in + S2b's note so nothing is re-surveyed. +- The legacy pause-field deletion (S0-pause, `one-core-phase-1.md:1130-1139`) runs as its + own slice in parallel with Phase 2, not inside the integration merge. + +## Corrections to the umbrella + +Surveyed 2026-09-12; `one-core.md` §Phase 2 is edited to match. Trust these over the +original wording. + +1. **Eight component caches share the walk, not four.** + `common/components/{transcript,subs,posts,digest,summaries,stats,duplicates,aliases}Cache.ts` + — 825 lines, all keyed by `OriginId` (`components/originId.ts`). Seven more walk sites: + `export/app/lib/offlineCache.ts`, `umtool/report-to-video/cues.mjs`, + `common/lib/siteRegistry.ts:287`, `export/app/lib/SearchDataContext.tsx`, + `export/app/lib/SearchSessionContext.tsx`, `export/app/lib/useAskChat.ts`, and the + deliberate copy in `export/app/lib/searchIndex.worker.ts:39-41` (a worker cannot import + the reader; it **stays**, guarded by the SW/contract equality test in S2a). +2. **The reader interface is mcp's 18-member `ShardSource`** (`mcp/src/source.ts:292-372`), + not the umbrella's four methods. `record(layer, slug, id)` must **not** be added: a + per-record fetch is a bench regression by construction (the walk is manifest → shard → + record, and callers already hold the shard). +3. **`config.dataDir` is not `reader-fs.ts`'s concern.** `LocalSource` reads a *composed* + public dir, which has no `dataDir`. The resolver the umbrella asked for belongs to + `cues.mjs:239-243`, which walks `channels/<slug>/data/` — that is S2b. + +## Layout and the browser/node rule + +``` +common/lib/archive/ + contract.ts re-exports CONTRACT (lib/corpus.ts:27-48) + pageFileName (lib/manifest.ts:44-46); + adds corpusUrl, manifestUrl(layer, slug, base?), pageUrl, rootFileUrl, ROOT_FILES, + shipsPwa(site); owns the hub entry type (HubSiteEntry / HubMemberInput / + HubSite / HubCorpusSite → one) + io-stats.ts recordRead / ioStatsSnapshot; guard + `typeof process !== "undefined" && process.env?.MCP_IO_STATS` + reader.ts interface ArchiveReader (= ShardSource verbatim), RemoteSource, PageCache, + PromiseMap — ZERO node imports + reader-fs.ts LocalSource — the only node:fs / node:path file; reader.ts never + value-imports it + reader-hub.ts HubSource +``` + +- `./lib/*` is wildcarded in `common/package.json:34`, so no exports line is needed for + `lib/archive/*` or `lib/search/*`. A new `components/*.ts` **would** need one — S2a + creates none. +- `mcp/src/source.ts` becomes a ~40-line re-export (`export type ShardSource = + ArchiveReader`, plus the three constructors), so its seven importers and `mcp/bench` + (spawns the server; never imports `source.ts`) are untouched. +- `manifestUrl` must reproduce `buildSiteCorpus` (`corpus.ts:251-258`; `join` at + `:216-219`): absolute when `siteUrl` is set, root-relative otherwise. The posts/digests + conditional spreads stay in `corpus.ts` — `corpus.json` output does not change. +- **`pnpm --filter export exec next build` is the only test of browser-safety.** tsc + cannot tell a `node:fs` import from any other; the export bundle can. Every slice runs it. + +## Dependency graph + +``` +S1 contract + reader + hub types + stubs ← the only prerequisite (merged to main first) + ├── S2a eight caches + offlineCache + SW guard test + ├── S2b cues.mjs reachability fix (plain node; no tsx) + ├── S2c shipsPwa / _headers / hub-entry dedupe + ├── S3 common/lib/search/ + the lib→components inversion + └── S0-pause (legacy pause fields, one-core-phase-1.md:1130-1139) +``` + +Every parallel slice branches off S1's merged tip on `main`. S1 pre-creates every module +the others fill as `export {}` stubs with the intended signatures in comments — the five +`archive/*` files and `lib/search/{policy,evalTree,window,rank,collapse}.ts` — so no two +slices create the same file. + +Hotspots, and who owns each: + +| file | owner | why | +|---|---|---| +| `common/package.json` exports | nobody | S2a creates no new `components/*.ts` | +| `common/lib/corpus.ts` | S1 only | it moves the hub types, so S2c never opens it | +| `common/bin/compose-site.ts`, `compose-hub.ts` | S2c only | | +| `mcp/src/source.ts` | S1 only | | +| `mcp/src/search.ts` | S3 only | | +| `common/lib/settings.ts`, `laneMigration.ts`, `SiteSettings` | S0-pause only | zero overlap with S1–S3 | + +## Slices + +### S1 — contract, reader, hub types, stubs + +Files: `common/lib/archive/*`, `common/lib/corpus.ts` (URL functions + hub types moved), +`mcp/src/source.ts` → re-export, the stubs. + +Commits, in order: (1) `contract.ts` + `io-stats.ts` + the `corpus.ts` URL functions; +(2) the move of `source.ts` into `reader.ts` / `reader-fs.ts` / `reader-hub.ts` with the +node imports isolated; (3) `mcp/src/source.ts` as the re-export; (4) the stubs. + +Tests to ADD: `archive/reader.test.ts` over an in-memory `ArchiveReader` (`Map`-backed): +the manifest/page walk, `PromiseMap` sharing (one fetch for concurrent callers), LRU +eviction, cached-404-as-null for digests. `archive/contract.test.ts` for both URL shapes +(absolute with `siteUrl`, root-relative without). + +S1 also commits the composed fixture site under `plans/tools/` (see Verification) so the +parallel slices reuse it instead of each rebuilding it. + +### S2a — the eight caches, `offlineCache`, the SW guard + +The eight caches become `memo(originId, () => reader.X(...))` — the memo stays, the walk +goes. `offlineCache.ts` enumerates every layer that has a manifest plus `ROOT_FILES` +(`/duplicates.json`, stats) — this closes the duplicates-page-dead-offline gap. +`export/public/site-sw.js:27` `SHARD_RE` and the evict prefixes (`:138-142`) widen to +match. The SW layer list stays hand-written (a service worker cannot import), guarded by a +`contract.test.ts` case that reads both SW files and asserts equality with +`CONTRACT.layers`. + +Not in scope: refactoring `SearchDataContext` / `SearchSessionContext` / `siteRegistry` +state. They keep their state; only the fetch walk moves. + +Tests to ADD: one wrapper test per cache shape — same promise on a hit, no double fetch on +a miss. + +### S2b — `cues.mjs` stops swallowing ENOENT + +`fromLocal` (`cues.mjs:239-243`) currently swallows a missing `channels/<slug>/data/` and +falls through to HTTP, which is how a relocated channel silently cuts from the wrong +cues. Replicate `assertChannelMediaReachable`'s checks (`common/lib/channelMedia.ts:319`) +in ~15 lines of plain `.mjs`: read `dataDir` from `config.json`, resolve it the way the +editor does, **throw** when unreachable. Cross-reference from a comment in +`channelMedia.ts` so the next change to the check finds its twin. Leave `pageFileName` / +`pageUrlFrom` alone. + +Record in the slice note the full `tsx` change list for Phase 5: six shebangs; +`driver.mjs:70,93,99,105,140,167`; `umtool.mjs:516`; `clip-bench.spec.ts:146`; root +`test:scripts`; the missing `dependencies` field in `umtool/package.json`. + +Tests: new `cues.test.mjs` cases. It stubs `fetchImpl` and asserts the fetch sequence, so +"unreachable throws instead of fetching" is directly assertable. + +### S2c — `shipsPwa`, `_headers`, one hub entry type + +One `shipsPwa(site)` in `contract.ts` replaces `compose-site.ts:207-209` and +`export/app/lib/mode.ts:26-29` (which passes `currentSite()`). One generated `_headers` +block replaces `compose-site.ts:62-85` and `compose-hub.ts:29-48` **and adds** `/digests/*` +and `/duplicates.json` to the CORS set — that is a wire change: its own commit, with a +before/after `curl -I` in the commit body. One hub entry type (S1 moved it to +`contract.ts`; S2c deletes the local copy in `compose-hub.ts:53`). + +Files: `compose-site.ts`, `compose-hub.ts`, `export/app/lib/mode.ts`. + +### S3 — one search pipeline + +`common/lib/search/`: + +- `policy.ts` — `MAX_PAGES 400`, `HARD_VIDEO_CAP 2000`, `WINDOW_LINE_CAP 200`, `truncate + 240` from `mcp/src/search.ts:259-268,456` exported as `MCP_POLICY`; the viewer passes + its own, uncapped. +- `evalTree.ts` — `search.ts:1189-1338` plus `lib/searchEval.ts`'s copy, once. +- `window.ts` — over `lib/transcriptWindow.ts`. +- `rank.ts`, `collapse.ts` — `search.ts:477` + `SearchResults.tsx:502`. + +Fetchers are INJECTED (`{ reader, policy, onProgress }`) so react-query stays in +`components/`; reuse `lib/concurrency.ts` `mapConcurrent`. `components/searchPipeline.ts` +keeps the react-query controllers and `runLeafPipeline` wraps `lib/search/`. +`lib/searchEval.ts:35-38` imports `lib/search/` instead of `components/searchPipeline` — +and then `"components"` is added to `FORBIDDEN.lib` in `common/architecture.test.ts:31-35` +(re-grep for other `lib → components` imports first; that inversion is the slice's +deliverable, not a test to bend). + +`mcp/src/search.ts` keeps every export its 13 tools use and becomes orchestration; caps +and concurrency are passed, not re-derived, so the bench's structural counts are unchanged +by construction. `mcp/src/search.test.ts` and `scanPlan.test.ts` stay in place. + +Tests to ADD: `lib/search/*.test.ts` ported from `search.test.ts` cases over the in-memory +reader. + +### S0-pause — the legacy pause fields (parallel, not Phase 2) + +Delete `transcriptionsPaused`, `downloadsPaused`, `digest.digestsPaused`, +`backfill.enabled` and `legacyGateHeld` per `one-core-phase-1.md:1130-1139`. Files: +`common/lib/settings.ts`, `common/lib/laneMigration.ts`, `SiteSettings`. Precondition, +re-checked after the first boot of merged code: all four `autoQueue.<lane>.held` keys +present in the live `settings.json` (true on 2026-09-12: transcription/download +`held: true`, digest/backfill `held: false`). Own review. + +## Invariants + +Phase 2 burns none of the 11 architecture allow-list entries and adds none. It must not: + +- touch `buildIndex.ts`'s writer walk; +- change any URL shape (the export e2e route-mocks shard URLs at + `export/e2e/helpers.ts:87-90`); +- change `corpus.json` output or bump any `CONTRACT` version; +- add a per-record fetch; +- let `reader-fs.ts` be reachable from a client module. + +## Verification + +Every slice: + +- `pnpm -r exec tsc --noEmit`; common tests (the architecture test included); mcp tests + (205); `pnpm test:scripts`; **`pnpm --filter export exec next build`**. +- `pnpm --filter yt-dlp-transcript-mcp bench --json` before and after, with the structural + read/byte counts diffed (wall ms is noise). +- `compose-site` on the FACTS.md fixture recipe — copy + `editor/e2e/fixtures/test-transcripts/one-youtube-channel-with-data/channels`, hand-write + `sites/testsite/site.json`, `build-index.ts` then `compose-site.ts` — byte-identical + modulo `generatedAt`: 16 files under `public/`, 7 under `index/`. S1 commits the composed + fixture under `plans/tools/`; the others diff against it. + +Then, behind the e2e queue lock from a worktree: export e2e + hub + 2origin (S1, S2a, S2c, +S3); the editor `export-search` and `export-player-platform-cache` specs (S2a, S3); umtool +e2e (S2b). + +Live contract: `plans/tools/jeralyzer-corpus-2026-09-12.json` is +`curl -s https://jeralyzer.pages.dev/corpus.json` taken before S1 (12,380 bytes, spec 3, +30 channels). After S3, a rebuilt jeralyzer's `corpus.json` diffs against it in nothing but +`generatedAt` and totals. + +## Cadence + +S1 = one implementer + one reviewer, merged to `main`. Then S2a, S2b, S2c, S3 and S0-pause +as parallel implementers on separate branches off that tip, two to three wide per the +session budget, each reviewed; e2e stays serial under the queue lock. Standard prompt +rules: fish `commit -F`, trailers, never boot against `transcripts/`, detached e2e + +Monitor, commit incrementally, add by path, never `next build` in the primary checkout. + +## Record + +Filled in as slices ship: sha range, actual gate numbers, every divergence from this plan. diff --git a/plans/one-core.md b/plans/one-core.md @@ -258,31 +258,45 @@ after relocate (a focus means more downloading, onto a disk that must have room) (which touches none of `autoQueue`, `syncScheduler` or `/channels`), branched off this branch's tip so it never edits the lane trees beside Phase 1. -### Phase 2 — The contract: one `ArchiveReader` (3 slices) +### Phase 2 — The contract: one `ArchiveReader` (5 slices) + +Slice plan: [`one-core-phase-2.md`](one-core-phase-2.md). Corrected 2026-09-12 after a +survey; the original wording is kept in git. 1. **`common/lib/archive/`**: `contract.ts` (re-exports the `CONTRACT` object from phase 0 - plus the walk as *code*: `manifestUrl(layer, slug)`, `shardUrl(...)`, `recordFor(id)`), - `reader.ts` (the `ArchiveReader` interface — `corpus()`, `manifest(layer, slug)`, - `shard(layer, slug, n)`, `record(layer, slug, id)` — and the HTTP transport, - browser-safe), `reader-fs.ts` (node), `reader-hub.ts` (federation). The implementation is - `mcp/src/source.ts` moved, caches included; mcp's `ShardSource` becomes a re-export. - `reader-fs.ts` should take `config.dataDir` into account where it exists — that is option A - of the interlude's plan, a resolver layered on top of the symlink rather than instead of it. -2. **Every reader uses it.** `common/components/{transcript,subs,posts,digest}Cache.ts` - become thin memo wrappers over the HTTP reader; `export/app/lib/offlineCache.ts` keys its - cache off `contract.ts` paths (this also fixes the open `duplicates.json` offline gap); - `umtool/report-to-video/cues.mjs` imports the reader — the package declares - `yt-dlp-transcript-common` and its bins run under `tsx` (a shebang change; umtool's - driver invokes them by path today). `shipsPwa()`, the `_headers` block and - `HubSiteEntry` are defined once in `contract.ts` / `publish`. -3. **One search pipeline.** The ranking, windowing and snippet logic in + plus the walk as *code*: `corpusUrl`, `manifestUrl(layer, slug, base?)`, `pageUrl`, + `rootFileUrl`, `ROOT_FILES`, `shipsPwa(site)`, and the one hub entry type), + `reader.ts` (the `ArchiveReader` interface — mcp's 18-member `ShardSource` + verbatim, **not** four methods, and **no** `record(layer, slug, id)`: a per-record + fetch is a bench regression — plus the HTTP transport, browser-safe, zero node + imports), `reader-fs.ts` (node — the only `node:fs` file), `reader-hub.ts` + (federation). The implementation is `mcp/src/source.ts` moved, caches included; mcp's + `ShardSource` becomes a re-export. `config.dataDir` is **not** `reader-fs`'s concern — + `LocalSource` reads a composed public dir; the resolver belongs to `cues.mjs`, below. +2. **Every reader uses it.** *Eight* component caches, not four — + `common/components/{transcript,subs,posts,digest,summaries,stats,duplicates,aliases}Cache.ts` + — become thin memo wrappers over the HTTP reader; `export/app/lib/offlineCache.ts` keys + its cache off `contract.ts` paths (this also fixes the open `duplicates.json` offline + gap) and the service worker's hand-written layer list is pinned to `CONTRACT.layers` by + a test. The deliberate copy in `searchIndex.worker.ts` stays (a worker cannot import). +3. **`cues.mjs` resolves `dataDir` and refuses to fall through.** umtool's cue resolver + replicates the editor's reachability check in plain `.mjs` and throws on an unreachable + channel instead of silently cutting from HTTP cues. It does **not** adopt `tsx` or + import the reader in this phase — that is Phase 5, and the change list is recorded in + the slice note. +4. **`shipsPwa()`, the `_headers` block and the hub entry type are defined once** in + `contract.ts` / `compose-*`. The generated `_headers` also gains CORS for `/digests/*` + and `/duplicates.json` (a wire change, its own commit). +5. **One search pipeline.** The ranking, windowing and snippet logic in `mcp/src/search.ts` and `common/components/searchPipeline.ts` become one - `common/lib/search/` module with no React; `SearchSessionContext` and the MCP tools both - call it. `mcp/bench/bench.ts` is the regression gate. - -Deleted: four hand walks of the shard scheme, mcp's private reader, one of two search -pipelines. Verified against the live contract: `curl https://jeralyzer.pages.dev/corpus.json` -before and after, byte-identical output from `compose-site` on the fixture corpus. + `common/lib/search/` module with no React and injected fetchers; `SearchSessionContext` + and the MCP tools both call it. `mcp/bench/bench.ts` is the regression gate, and + `lib/ → components/` joins the architecture test's forbidden edges. + +Deleted: every hand walk of the shard scheme but the worker's, mcp's private reader, one of +two search pipelines. Verified against the live contract: `curl +https://jeralyzer.pages.dev/corpus.json` before and after, byte-identical output from +`compose-site` on the fixture corpus. ### Phase 3 — Views in the core, editor as shell (4 slices) diff --git a/plans/tools/jeralyzer-corpus-2026-09-12.json b/plans/tools/jeralyzer-corpus-2026-09-12.json @@ -0,0 +1 @@ +{"spec":3,"kind":"site","generatedAt":"2026-08-21T12:49:50.160Z","generator":"Archilyzer (https://archilyzer.pages.dev)","site":{"id":"jeralyzer","title":"Jeralyzer","description":"Browse and search Jeremy Hambly's video transcripts","url":"https://jeralyzer.pages.dev"},"totals":{"channels":30,"videos":31104},"channels":[{"slug":"thequartering-X","name":"@TheQuartering on X","videoCount":0,"groupId":"default","postCount":313,"manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/thequartering-X/manifest.json","subs":"https://jeralyzer.pages.dev/subs/thequartering-X/manifest.json","posts":"https://jeralyzer.pages.dev/posts/thequartering-X/manifest.json"}},{"slug":"chrissie-mayr","name":"Chrissie Mayr","videoCount":3521,"groupId":"other","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/chrissie-mayr/manifest.json","subs":"https://jeralyzer.pages.dev/subs/chrissie-mayr/manifest.json"}},{"slug":"redbar","name":"Chrissie on Redbar (KC Restoration Project)","videoCount":25,"groupId":"other","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/redbar/manifest.json","subs":"https://jeralyzer.pages.dev/subs/redbar/manifest.json"}},{"slug":"clawstruck","name":"ClawStruck","videoCount":270,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/clawstruck/manifest.json","subs":"https://jeralyzer.pages.dev/subs/clawstruck/manifest.json"}},{"slug":"community-notes","name":"Community Notes","videoCount":39,"groupId":"default","digestCount":39,"manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/community-notes/manifest.json","subs":"https://jeralyzer.pages.dev/subs/community-notes/manifest.json","digests":"https://jeralyzer.pages.dev/digests/community-notes/manifest.json"}},{"slug":"cornbreadman","name":"cornbreadman (Archive)","videoCount":337,"groupId":"archives","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/cornbreadman/manifest.json","subs":"https://jeralyzer.pages.dev/subs/cornbreadman/manifest.json"}},{"slug":"elissa-clips","name":"Elissa Clips","videoCount":246,"groupId":"archives","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/elissa-clips/manifest.json","subs":"https://jeralyzer.pages.dev/subs/elissa-clips/manifest.json"}},{"slug":"exclusively-games","name":"Exclusively Games","videoCount":98,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/exclusively-games/manifest.json","subs":"https://jeralyzer.pages.dev/subs/exclusively-games/manifest.json"}},{"slug":"friday-night-tights","name":"Friday Night Tights","videoCount":313,"groupId":"other","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/friday-night-tights/manifest.json","subs":"https://jeralyzer.pages.dev/subs/friday-night-tights/manifest.json"}},{"slug":"friendofrc","name":"FriendOfRC","videoCount":63,"groupId":"default","digestCount":63,"manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/friendofrc/manifest.json","subs":"https://jeralyzer.pages.dev/subs/friendofrc/manifest.json","digests":"https://jeralyzer.pages.dev/digests/friendofrc/manifest.json"}},{"slug":"geeks-and-gamers-fnt","name":"Geeks & Gamers FNT","videoCount":13,"groupId":"other","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/geeks-and-gamers-fnt/manifest.json","subs":"https://jeralyzer.pages.dev/subs/geeks-and-gamers-fnt/manifest.json"}},{"slug":"hex-headquarters","name":"HexHeadQuarters","videoCount":16,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/hex-headquarters/manifest.json","subs":"https://jeralyzer.pages.dev/subs/hex-headquarters/manifest.json"}},{"slug":"jeremy-hambly","name":"Jeremy Hambly","videoCount":2231,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/jeremy-hambly/manifest.json","subs":"https://jeralyzer.pages.dev/subs/jeremy-hambly/manifest.json"}},{"slug":"melonie-mac","name":"Melonie Mac","videoCount":181,"groupId":"other","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/melonie-mac/manifest.json","subs":"https://jeralyzer.pages.dev/subs/melonie-mac/manifest.json"}},{"slug":"melonie-mac-gaming","name":"Melonie Mac (Gaming Streams)","videoCount":269,"groupId":"other","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/melonie-mac-gaming/manifest.json","subs":"https://jeralyzer.pages.dev/subs/melonie-mac-gaming/manifest.json"}},{"slug":"mevsme","name":"MeVsMe","videoCount":10,"groupId":"default","digestCount":9,"manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/mevsme/manifest.json","subs":"https://jeralyzer.pages.dev/subs/mevsme/manifest.json","digests":"https://jeralyzer.pages.dev/digests/mevsme/manifest.json"}},{"slug":"midwestly","name":"MidWestly","videoCount":773,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/midwestly/manifest.json","subs":"https://jeralyzer.pages.dev/subs/midwestly/manifest.json"}},{"slug":"nerdrotic-live","name":"NerdroticLive","videoCount":174,"groupId":"other","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/nerdrotic-live/manifest.json","subs":"https://jeralyzer.pages.dev/subs/nerdrotic-live/manifest.json"}},{"slug":"pokemonheadquarters","name":"PokemonHeadQuarters","videoCount":86,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/pokemonheadquarters/manifest.json","subs":"https://jeralyzer.pages.dev/subs/pokemonheadquarters/manifest.json"}},{"slug":"quartering-live","name":"Quartering Live","videoCount":855,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/quartering-live/manifest.json","subs":"https://jeralyzer.pages.dev/subs/quartering-live/manifest.json"}},{"slug":"quarteringvlogs","name":"QuarteringVlogs","videoCount":9,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/quarteringvlogs/manifest.json","subs":"https://jeralyzer.pages.dev/subs/quarteringvlogs/manifest.json"}},{"slug":"rcflightschool","name":"RCFlightSchool","videoCount":72,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/rcflightschool/manifest.json","subs":"https://jeralyzer.pages.dev/subs/rcflightschool/manifest.json"}},{"slug":"rcnightmare","name":"RCNightmare","videoCount":463,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/rcnightmare/manifest.json","subs":"https://jeralyzer.pages.dev/subs/rcnightmare/manifest.json"}},{"slug":"rcspotlight","name":"RCSpotlight","videoCount":2,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/rcspotlight/manifest.json","subs":"https://jeralyzer.pages.dev/subs/rcspotlight/manifest.json"}},{"slug":"steven-crowder","name":"Steven Crowder","videoCount":2,"groupId":"guest-appearances","digestCount":2,"manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/steven-crowder/manifest.json","subs":"https://jeralyzer.pages.dev/subs/steven-crowder/manifest.json","digests":"https://jeralyzer.pages.dev/digests/steven-crowder/manifest.json"}},{"slug":"teamrcn","name":"TeamRCN","videoCount":7,"groupId":"default","digestCount":7,"manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/teamrcn/manifest.json","subs":"https://jeralyzer.pages.dev/subs/teamrcn/manifest.json","digests":"https://jeralyzer.pages.dev/digests/teamrcn/manifest.json"}},{"slug":"the-incredible-salt-mine","name":"TheIncredibleSaltMine","videoCount":171,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/the-incredible-salt-mine/manifest.json","subs":"https://jeralyzer.pages.dev/subs/the-incredible-salt-mine/manifest.json"}},{"slug":"the-quartering","name":"TheQuartering","videoCount":11336,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/the-quartering/manifest.json","subs":"https://jeralyzer.pages.dev/subs/the-quartering/manifest.json"}},{"slug":"the-quartering-rumble","name":"TheQuartering (Rumble)","videoCount":7925,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/the-quartering-rumble/manifest.json","subs":"https://jeralyzer.pages.dev/subs/the-quartering-rumble/manifest.json"}},{"slug":"unsleeved-media","name":"UnsleevedMedia","videoCount":1597,"groupId":"default","manifests":{"transcripts":"https://jeralyzer.pages.dev/transcripts/unsleeved-media/manifest.json","subs":"https://jeralyzer.pages.dev/subs/unsleeved-media/manifest.json"}}],"shardScheme":{"description":"Transcripts are served as paginated JSON shards — there is no per-video file. To read one video's transcript: (1) GET the channel's transcripts manifest; (2) look up the video id in its `slugToPage` map to get a page number N; (3) GET page-<NNNN>.json (N zero-padded to 4 digits) and take the record whose `id` matches.","transcriptsManifest":"<channel.manifests.transcripts> -> { pageCount, slugToPage: { <videoId>: <pageNumber> } }","transcriptPage":"/transcripts/<slug>/page-<NNNN>.json -> array of { id, title, uploadDate, duration, channel, description, tags, webpageUrl, platform, cues: [{ start, end, text }] }","subsManifest":"<channel.manifests.subs> -> lighter list-view records under the same slugToPage scheme","summariesIndex":"/summaries/manifest.json + /summaries/page-<NNNN>.json -> cross-channel browse index","pageNumberFormat":"zero-padded to 4 digits, e.g. page 0 -> page-0000.json"},"useWithAi":"https://jeralyzer.pages.dev/use-with-ai","postScheme":{"description":"Social posts (X/Twitter, Bluesky) are archived as a PARALLEL corpus to video transcripts and are served as paginated JSON shards under the same scheme: (1) GET the channel's posts manifest; (2) look up the post id in its `slugToPage` map to get a page number N; (3) GET page-<NNNN>.json and take the record whose `id` matches. Only channels whose source is a social account have a posts manifest.","postsManifest":"<channel.manifests.posts> -> { pageCount, slugToPage: { <postId>: <pageNumber> } }","postPage":"/posts/<slug>/page-<NNNN>.json -> array of { id, slug, channelSlug, author, authorName, createdAt, uploadDate, text, url, platform, threadId, replyTo, quoted, repostOf, isReply, isRepost, links, mediaCount, engagement }","ordering":"newest first, by `createdAt` (ISO-8601, ms precision where the source provides it)","dateFilter":"`uploadDate` (YYYYMMDD, derived from createdAt) is carried on every post so the same date filters work across transcripts and posts","permalink":"each post carries its own canonical `url`; no timestamp fragment applies"},"digestScheme":{"description":"AI digests are chapters and topic tags DERIVED from a video's transcript by a local model, composed with any human corrections before publication. They are served as paginated JSON shards under the same scheme: (1) GET the channel's digests manifest; (2) look up the video id in its `slugToPage` map to get a page number N; (3) GET page-<NNNN>.json and take the record whose `id` matches. THE LAYER IS SPARSE: a video id absent from `slugToPage` has no digest, and a channel with no digests has no manifest at all. Absence means 'not yet generated', never 'nothing to say'.","digestsManifest":"<channel.manifests.digests> -> { pageCount, slugToPage: { <videoId>: <pageNumber> } }","digestPage":"/digests/<slug>/page-<NNNN>.json -> array of { id, slug, generatedAt, chapters: [{ id, start, clock, title, decidedBy }], tags: [{ id, tag, decidedBy }], provenance, derivedFrom? }","chapterTiming":"`start` is SECONDS, already snapped to a real transcript cue boundary — use it to seek. `clock` is the raw HH:MM:SS string the model emitted, kept for auditing; do not parse it for timing.","attribution":"`decidedBy` is \"ai\" or \"human\" per item, so machine output and human corrections stay distinguishable after composition.","derivedFrom":"When present, this digest was generated for a DIFFERENT video (the canonical member of a duplicate cluster) and shared onto this one; it names that video and the measured cue-timing offset in seconds. Treat its chapter titles as describing the canonical upload."},"bulkArchives":{"manifest":"https://jeralyzer.pages.dev/archives/manifest.json","note":"Whole-channel transcript and live-chat archives for offline bulk ingestion."}} +\ No newline at end of file