Repository navigation
seed data unification
Status: Landed. Phase 1 (seed compiler) + Phase 2 (seed import seam, default-on — opt out via localStorage['wodwiki.seedImport.enabled'] = '0') + Phase 3 (all read surfaces flipped corpus by corpus: efforts → syntax → dashboards → collections/feeds → canvas → workoutIndex/page-examples, and the corpus block_index folded into the seed) + Phase 4 (cleanup: glob loaders, staticBlockIndex JSON generation, and the effort-markdown bundled tier deleted; Storybook needs no changes — it never imported the loaders). Lang's data/bundled-efforts static fallback remains by design: it backs the analytics engine's effort lookup (createAnalyticsEngineForBlock), not a content-loading path.
Date: 2026-09-07
Related: docs/05-architecture.md (stale repo layout), CONTEXT.md § Persistence & storage, § Identity & result recording
One runtime data source. Today bundled content reaches the app through ~10 separate build-time markdown loaders while user content lives in IndexedDB, and analytics joins across both via a third, generated JSON corpus. This proposal collapses all of that into a Seed pipeline:
- Local
markdown/is the seed source — compiled at build time into artifacts a static host can serve and the app imports intowodwiki-db. - After the seed is imported, every runtime read loads from IndexedDB. No markdown loader runs at runtime.
- The compiled seed is timestamped and versioned. On boot the app re-imports when the server publishes a newer seed than what Storage holds.
- Hosting stays static (GitHub Pages today): the seed is plain files under the web root — no server code.
server/remains the cast relay only.
| # | Path | Modules | Goes to |
|---|---|---|---|
| 1 |
markdown/canvas/**/*.md glob |
app/canvas/canvasRoutes.ts, src/repositories/page-examples.ts, src/repositories/script-loader.ts
|
in-memory strings, parsed per render |
| 2 |
markdown/collections/** + feeds/** globs |
script-collections.ts, script-feeds.ts, script-groupings.ts
|
in-memory |
| 3 |
markdown/dashboards/** glob |
app/lib/dashboardCorpus.ts |
in-memory, read-only until cloned |
| 4 |
markdown/efforts/** glob (+ bun fs fallback) |
src/repositories/effort-markdown.ts |
CompositeEffortRegistry bundled tier |
| 5 |
markdown/** glob (all) |
app/lib/workoutIndex.ts |
"view source" resolution |
| 6 | syntax guides, page template (?raw imports) |
src/content/syntaxGuideReference.ts, app/templates/defaultPlaygroundContent.ts
|
in-memory |
| 7 | generated static-block-index.json (~8 MB, 21k rows) |
src/services/content/staticBlockIndex.ts → queryService.ts
|
in-memory rows joined with IDB |
| 8 |
wodwiki-db v29 |
IndexedDBService — notes (whole source + occurrence map), block_revisions, sessions, attachments, events, block_index, block_efforts, field_catalog, efforts |
user data |
| 9 |
wodwiki-user-calcs, wodwiki-telemetry (separate DBs) |
app/stores/userCalcStore.ts, telemetry/TelemetryService.ts
|
user data / telemetry |
| 10 | localStorage (~10 keys), user file import, ?zip= share links, cast RPC |
various | mixed small state |
Corpus size: 14 MB raw across 886 files (collections 13 MB, canvas 384 KB, efforts 256 KB, feeds 108 KB, dashboards 28 KB).
- No locality for "bundled content." Adding one markdown file touches nothing, but changing how bundled content is found, parsed, or routed means editing ten loaders. The deletion test fails for each loader: delete it and its logic reappears scattered across callers.
-
One logical question, two stores. WQL content search (
queryService.ts) reads IDBblock_indexfor user notes and the generated JSON for the corpus — the join key (sourceId, Block Content Id) is duplicated knowledge. -
Two-tier Effort merge is implemented twice:
CompositeEffortRegistry(lang package) and the ad-hoc IDB-first-then-bundled merge inapp/hooks/useEffortContent.ts. - Dashboard Notes are "read-only until cloned into the vault" — a clone-by-copy semantic that exists only because the seed lives outside Storage.
-
resetUserDatamust know three stores (main DB + localStorage + sessionStorage) — coordination cost paid at every wipe path. - Offline ≠ free. The bundle already carries all 14 MB, but there is no single place that answers "what bundled content does this install have?"
Seed — the immutable, timestamped compilation of the bundled content corpora (Catalogs, Canvas pages, syntax guides, Efforts, Dashboard Notes, page template) into versioned artifacts that the app imports into Storage. The Seed is the only path bundled content takes into IndexedDB. Seed rows are owned by the importer until the user edits them.
New term — proposed for CONTEXT.md § Persistence & storage once this direction is accepted (see § Vocabulary).
Takes markdown/** + templates; emits apps/playground/public/seed/:
public/seed/
├── manifest.json ← small, no-cache
└── chunks/
├── canvas.<sha256-8>.json ← immutable, content-hashed
├── collection.crossfit-girls.<sha256-8>.json
├── feed.<slug>.<sha256-8>.json
├── efforts.<sha256-8>.json
├── dashboards.<sha256-8>.json
├── syntax.<sha256-8>.json
├── block-index.<n>.<sha256-8>.json ← precomputed BlockIndexRow[] (split, ~8 MB)
└── block-efforts.<n>.<sha256-8>.json ← precomputed BlockEffort[] (exercise containment index)
-
Derives at build time what loaders derived at runtime: typed frontmatter (
domain,format,equipment,quality,intentviaextractTypedFrontmatterTags), route slugs, exercise containment (block_efforts), derived first-published dates, canvas page metadata. Runtime receives ready-to-store rows, with markdown content preserved byte-faithfully. -
manifest.json:
- Reuses existing precedent:
scripts/generate-static-block-index.tsbecomes a stage inside the compiler;generate-static-shells.tsandstamp-version.tsshow the post-build wiring pattern (bun run buildextension). - Parity gate: the build fails if any file matched by today's globs is missing from the seed — the compiler cannot silently drop content.
One interface, real adapters — the seam exists because the implementations genuinely vary:
| Adapter | Used by |
|---|---|
HttpSeedSource — fetches manifest + chunks from /seed/
|
production (static host) |
InMemorySeedSource — fixtures |
unit tests, Storybook |
EmbeddedSeedSource — a small bootstrap chunk baked into the bundle |
first paint / offline fallback (see Open questions) |
SeedImporter.apply(manifest, chunkLoader) writes through the existing Storage layer in per-chunk transactions:
- Materializes corpus items into Storage: Notes (deterministic UUID id from path, title, date, catalog) holding byte-faithful markdown in
rawContentplus the occurrenceblocksmap, initial BlockRevision v1 rows for actionable blocks, Pages (slug/date anchors viapage_notes), Tags & NoteTags (typed viaDEFAULT_TAG_TYPES), Efforts (registrySource: 'bundled'), and current-onlyblock_index/block_effortsprojections. (Schema 8; superseded the retired per-segment rows and separate derived chunks.) -
Checkpoint record in a new
metakey-value store (V19):{ seedVersion, importedAt, chunks: { [id]: sha256 } }— per-chunk resume, same pattern asfield_catalog_metabackfill state. -
Idempotent: re-applying a chunk with a hash already recorded in
metais a no-op. Crash mid-import → next boot resumes at the first unrecorded chunk.
Seed versions are independent of the database schema version. Each source has its own seed:<sourceId> checkpoint and chunk hashes.
- Boot opens IndexedDB and reads the core checkpoint.
- On a first visit, only core imports automatically. The committed
canvaschunk makes the home page available while remaining core chunks continue importing. - On a return visit, cached content renders immediately. Core and subscribed sources fetch manifests in the background, even when the stored core version matches the embedded build version.
- Changed chunks import in transactions. An older published version never replaces a newer local copy. User-edited notes remain untouched.
- Completion invalidates content consumers, broadcasts to other tabs, and reports committed new/updated note counts. The header shows download activity; the completion toast links to
/catalogs.
The home Community feeds section offers CrossFit Programming and Dan John 40-Day Program as demos. Each subscribes to one feed.* chunk from the community repository, not the full corpus. npx wod build compiles a markdown repository into manifest/chunk artifacts; the repository's publishing pipeline deploys them to GitHub Pages. Manual successful sync, including an already-current checkpoint, subscribes the source. Explicit unsubscribe survives later launches. Existing imported library checkpoints become subscriptions only when no explicit choice exists.
Import coordination across tabs: claim via the meta record (single writer), notify via BroadcastChannel('wodwiki.seed') — same shapes as the existing wodwiki.analytics invalidation bus and the IDB upgrade yield logic in IndexedDBService.
One provenance rule replaces the per-corpus merge logic:
| Row state | Importer behaviour |
|---|---|
origin: 'seed', unmodified |
overwrite in place when its chunk hash changed; delete when the item vanished from the manifest |
user-edited seed item (origin flipped to 'user' on first edit) |
never touched by the importer |
| user-created rows (Notes, Results, attachments, events, calcs) | never touched |
Consequences:
-
Efforts: the two-tier merge (
CompositeEffortRegistry+ theuseEffortContentmerge) collapses into one provenance-aware read — bundled Efforts are rows in theeffortsstore; user edits fliporigin; resolution order is a query, not a registry composition. -
Dashboard Notes:
origin: 'seed'renders read-only (today's semantics); "clone into the vault" becomes "write a'user'-origin copy" — no special corpus machinery. -
Workout results and analytics are never seeded. The Analytics Store stays disposable and re-derivable from user data, per its contract in
CONTEXT.md.
Consumers stop importing markdown and ask Persistence (the domain layer over Storage, per CONTEXT.md) for content. Canvas route derivation reads seeded canvas Notes; "view source" reads the Note's rawContent; the Query Service reads block_index from IDB for all sources.
After the cutover this seam has two adapters — IDB (production) and InMemoryStorage (tests/Storybook) — which makes it a real seam by the two-adapter rule, and gives every content consumer a test surface that needs no Vite, no globs, no fixture files on disk.
| Module | Replaced by |
|---|---|
app/canvas/canvasRoutes.ts glob |
seeded canvas Notes + route derivation (canvasRouteLookup.ts pure logic stays) |
app/lib/dashboardCorpus.ts |
seeded Dashboard Notes |
app/lib/workoutIndex.ts |
Persistence read of the Note's raw content |
src/repositories/page-examples.ts |
Persistence query by corpus/source |
src/repositories/script-loader.ts |
— |
src/repositories/script-collections.ts / script-feeds.ts / script-groupings.ts
|
item shape + ordering precomputed into seed rows |
src/repositories/effort-markdown.ts bundled tier + fs fallback |
seeded efforts rows (keep the pure parse fns for the compiler) |
src/content/syntaxGuideReference.ts |
seeded syntax Notes |
src/services/content/staticBlockIndex.ts (8 MB lazy JSON) |
block_index populated by import; Query Service reads one store |
- Vite copies
public/seed/verbatim intodist/— works on GitHub Pages unchanged. -
manifest.json: served withcache: 'no-cache'fetch (and/or a deploy-injected?v=bust). Chunks: immutable filenames, far-future cache headers where the host allows. - Returning visitors probe core and subscribed manifests without blocking cached content. Unchanged chunk hashes prevent redundant downloads and writes.
-
Compiler only — emit
public/seed/, ship dark. Parity test vs. globs (file count + per-file sha256). -
Importer only — V19
metastore; import runs behind a flag into the live DB; reads still from globs. Validates ownership rules against real user data. -
Flip reads corpus by corpus, deleting each loader as it flips. Suggested order, smallest blast radius first: efforts → syntax → dashboards → collections/feeds (+ Query Service
block_index) → canvas → workoutIndex/page-examples. -
Cleanup — delete glob loaders,
staticBlockIndexJSON generation (folded into the compiler),CompositeEffortRegistrybundling,effort-markdownfs fallback. Storybook switches toInMemorySeedSourcefixtures.
- Parity: seed output ≡ glob corpus (counts + content hashes) — fails the build on drift.
-
Importer (on
InMemoryStorage): fresh import; re-import idempotency; newer-version re-import touches only changed chunks; user-edited rows survive; vanished seed rows are deleted only when unmodified; crash-resume from checkpoint. -
Version check: fake
SeedSourcewith stale / equal / newer / rolled-back manifests; embedded fast path skips fetch. - E2E: first visit imports core; returning cached content remains usable while manifests refresh; each demo imports only its chosen chunk; completion counts reflect committed notes.
- First-load network dependency. Today the corpus rides in the bundle; with the seed in IDB, a cold install must fetch before content routes render. Options: (a) embed a small bootstrap chunk (home + getting-started, ~tens of KB) — keeps first paint offline-safe, recommended; (b) block on full import with a loading route — simpler, worse UX. Decide in review.
- 8 MB block index import cost on low-end devices. Mitigation: chunk per catalog, import in background after first paint; content search over the corpus is degraded until done (today it pays the same 8 MB parse lazily, so this is a shift, not a regression).
- Storage quota (notably Safari): the corpus moves from memory/cache into quota-tracked IDB. Roughly net-neutral on total bytes (bundle shrinks as chunks are removed from it), but quota errors need a handling story for the importer.
-
Canvas pre-parsing scope: keep runtime page parsing (
parseCanvasMarkdown) on seeded raw markdown initially; pre-computing Page Blocks at compile time is a later optimization, not part of unification. -
wodwiki-user-calcs/wodwiki-telemetry: out of scope for seeding (user data and telemetry, different lifecycles) — consolidating the calcs DB intowodwiki-dbis a separate, optional cleanup.
- Seed — as defined above; § Persistence & storage.
-
Seed Import — the seam that pulls a manifest, applies changed chunks transactionally, and records
seedVersion; adapters: HTTP (static host), embedded bootstrap, in-memory (tests). -
Seed Origin — provenance on a row (
'seed' | 'user'): the single ownership rule deciding whether the importer may overwrite or delete it. Replaces the bundled/user tier language for Efforts and the read-only-until-cloned language for Dashboard Notes.
{ "version": 1770000000000, // monotonic: builtAt epoch ms "builtAt": "2026-09-07T10:00:00Z", "chunks": [ { "id": "efforts", "path": "chunks/efforts.ab12cd34.json", "sha256": "…", "bytes": 262144, "count": 87 } ] }