source-wfiba

A Clark View source val serving the 2026 FIBA Women's Basketball World Cup (2026-09-04 to 2026-09-14). It implements source protocol v1 and nothing else: one source, one competition, one upstream.

Split out of plusjade/source-sports, which still carries its own fiba competition. This val did not remove FIBA from that one — de-duplicating the two is a separate decision with its own migration.

Layout

PathRole
rpc.tsMount point. Bearer check, then serveSource. Also /health and /coverage.
wfibaSource.tsThe source definition: settings schema, parseSettings, read, one write.
Shared SDKsource-sdk, imported through source-sdk@12-main/mod.ts. No vendored SDK.
lib/store.tsThe only module that touches SQLite.
lib/espn.tsESPN scoreboard payload → rows. Pure.
lib/feed.tsSelection: each favorite's own next game.
lib/items.tsRows → the widget's item shape, including the startsAt/expiresAt window and the caption derived from it. Pure.
lib/dates.tsClient-offset date math. Pure.
lib/channels.tsBroadcaster labels. Pure.
lib/schema.sqlComplete schema and roster seed.

Storage

Two tables, both provisioned by tools/deploy-schema.ts at deployment — never by a request.

wfiba_games holds one row per game, with away_code and home_code as real columns. This is the deliberate departure from source-sports, which stores a whole day's ESPN scoreboard as one JSON blob in source_cache and can therefore only filter by team in TypeScript after loading and parsing every day. Here "which games involve Spain" is a WHERE clause against an index.

wfiba_teams is the 16-nation selectable roster: slug, display name, ESPN code, order. It is a table rather than a SELECT DISTINCT over the games so the settings form's choices do not depend on ingest state. Slugs match the sports val's catalog (spain, united-states, …), so a selection means the same thing in both.

Ingest

ESPN blocks Val Town's egress — 403 Access Denied regardless of headers, re-verified 2026-09-12 with a default fetch (earlier header variants tested Sep 7). So read never fetches; an external process does.

POST /v1/write with operation: "games.ingest" and {dateKey, payload}, where payload is ESPN's raw scoreboard JSON. The val does the shape translation, so the ingest process stays a dumb pipe. Each date's payload is authoritative: it replaces that date's rows rather than merging, so a rescheduled or cancelled game disappears instead of lingering on a widget. Idempotent — re-running is the intended way to refresh statuses.

tools/ingest.ts is that process. Run it off-platform:

deno run --allow-net --allow-env tools/ingest.ts

Operational status — 2026-09-12

Storage has 36 games across nine dates, ending Sep 13. The Sep 12 semifinals are now ingested: France–Germany at 14:30 UTC and Spain–USA at 18:00 UTC; the authenticated feed returned both, with TBS. Sep 9–10 participants/statuses were also refreshed. Inputs and ESPN-to-FIBA ID mapping: tools/espn-snapshot-20260912.json. The earlier manual FIBA capture remains in tools/fiba-snapshot-20260909.json. Preserve fiba-2026-game-{official game number} IDs when changing providers; the unchanged ESPN runner does not perform this mapping. Refreshed home/away fields follow ESPN's supplied roles, not the earlier first-listed-side convention.

The unchanged tools/ingest.ts was run in Val Town and failed before writes: ESPN 403 for 2026-09-04 (evaluation 01a09666-9e55-7069-92bc-7307b4d4d729). A separate Sep 12 request also returned 403. Local requests returned HTTP 200, including both semifinals: ESPN coverage is no longer limited to Sep 9. The successful refresh fetched ESPN off-platform and replayed reviewed payloads through the existing games.ingest endpoint; no browser interaction was needed. Temporary probe/replay scripts were removed.

Open: Sep 13 ESPN still returned two TBD-vs-TBD games at capture time, so the existing medal-game bracket placeholders were retained. Recheck after semifinals and replace the complete Sep 13 date when participants are published; verify the team-filtered read as well as stored coverage. Close this status when resolved.

Refresh procedure and automation assessment

Preflight the required dates before writing. Reject malformed/off-bucket data and review missing games or regressions from resolved teams to TBD before an authoritative date replacement. Do not blindly rerun tools/ingest.ts over hand-verified dates: it has no completeness guards or FIBA ID mapping. The script was left unchanged for the as-is evaluation.

Prefer accessible structured ESPN data when it covers the requested games. Use the official FIBA tournament page and its date controls when ESPN is incomplete, cross-checking client-local browser times against explicit GMT times. A manual browser fallback has been proven; an automated FIBA parser has not.

A scheduler on a tested external runtime could fetch ESPN and call /v1/write without computer use; the local fetch succeeded, but no unattended host was tested or scheduled. Direct Val Town cron execution currently encounters the same 403. If a browser fallback becomes necessary, a cron can orchestrate a remote browser (Val Town's Kernel guide); that needs a configured service and validated extraction, not this interactive session. Before scheduling, add stable-ID mapping, completeness checks, and visible failures. Recheck network access from the intended scheduler before selecting it.

Settings contract

GET /v1/descriptor publishes a closed object in the supported subset the parent's generic form renders: an array of 16 string choices carrying const/title/x-group. Only teams is accepted; unknown keys are rejected. Expiry visibility belongs to device Presentation in the parent. This source returns its selected candidates even after expiry. Captions follow the published window; stored status does not override it. capabilities.validateSettings is true and POST /v1/validate-settings runs the same parseSettings a read does, without reading or leaking parsed internal context.

Checks

The source check reads production data and removes its disposable fixture. All SDK imports use https://esm.town/v/plusjade/source-sdk@12-main/mod.ts; keep them on one snapshot and rerun this check on a branch when upgrading.

  • tools/source-contract-check.ts — 50 assertions over the whole HTTP surface, plus pinned pure-function behaviour (Unix seconds, the two-hour window and the captions derived from it, per-team selection, the ingest date filter) and a disposable row on 1900-01-01. Shared protocol conformance and boundary checks now live in plusjade/source-sdk/tools/sdk-check.ts.