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:
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