This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
gtfs-merge fetches GTFS Static + GTFS-Realtime feeds from N transit networks, namespaces every ID, merges them into one feed, synthesises parent stations across networks, and serves everything over HTTP. gtfs-merge-design.md is the authoritative spec (code comments reference its § numbers); this implementation covers milestones M0–M3. reunion.yaml is the production config for the six La Réunion networks and encodes real upstream quirks in its comments — don't simplify it blindly.
make build # CGO_ENABLED=0 build → ./gtfs-merge
go test ./... # full suite
go test ./internal/cluster/ -run TestMergeCloseSameName -v # single test
make run # daemon on :8080 (live upstream fetches)
make once # one-shot: fetch, build, write ./out/, exit — fastest end-to-end check
go vet ./...-once exits non-zero if any feed was excluded; use it to validate config or pipeline changes against the real feeds. The daemon hot-reloads config on SIGHUP.
Two independent loops, connected only by an atomic snapshot pointer:
- Static loop (
internal/app/static_manager.go→internal/static): per-feed fetch timers → disk cache (internal/fetch) → on content-hash change,static.Build()runs scan → cluster → plan → stream-write → publishes an immutablestatic.Snapshotviaatomic.Pointer. The snapshot holds the merged ZIP, per-feed ZIPs, cluster reports, and themodel.Index(ID sets + stop→parent map). - RT loop (
internal/app/rt_manager.go→internal/rt): one poller per (feed, URL). Pollers parse + prefix in place once, store into aSlot; the builder rebuilds on every poll notification, resolving against the currently published static Index (snapshot pinning — an RT snapshot records which static version it was resolved against).
Serving (internal/httpapi) is lock-free: handlers read the two atomic pointers and http.ServeContent over in-memory blobs. internal/status is the mutable state registry both loops write into; /status.json, the HTML dashboard and the Prometheus age/state collector are all derived from its Document().
Config reload works by rebuilding the whole application (managers + status store) behind another atomic pointer in cmd/gtfs-merge/main.go; the metrics registry survives reloads by reading the store through a provider function.
- ID scheme is frozen:
feedslug + ":" + originalID, applied to every ID column (internal/prefixholds the file→columns table) and every RT field (explicit field handling ininternal/rt/prefix.go, deliberately no reflection). Source IDs containing:(all alterneo stops) are NOT escaped — reversal is split-on-first-:against the prefix registry.P:is reserved for synthetic parents. - Merged ZIP is byte-deterministic for identical inputs (fixed zip mtimes, canonical file/column order, sorted iteration everywhere in the build path).
TestBuildDeterminismenforces this; never introduce map-iteration order or timestamps into anything that lands in the ZIP.feed_versionis derived from input hashes, not output bytes (avoids circularity). - Per-feed ZIPs are "prefixed, otherwise untouched" (design G4): no synthetic parents, no cluster reassignments — only prefixing plus legality fixes (synthesized
agency_idfor feeds lacking one, e.g. carsud). - RT builder must never mutate slot messages — they are shared across rebuilds; it
proto.Clones beforeResolve()filters. Pollers are the only writers to slots and prefix exactly once at parse time. - Fail soft: a feed that won't scan is excluded and reported (
DegradedFeeds), never fatal; stale RT sources synthesize alerts and are dropped from output afterdrop_after(empty valid feed rather than lies). - Clustering operates on station-level units (§7.1): upstream stations absorb children and are never split; two stations from the same feed never merge; the repair pass (lowest-edge deletion) enforces diameter/size caps against transitive drift — identity-only components are exempt, human overrides (
must_not_link) beat everything including identity. config.Loaduses strict YAML (KnownFields): new config keys require struct changes; durations/distances/sizes use the customDuration/Meters/ByteSizetypes.
internal/static/build_test.go is the integration surface: three synthetic in-memory feeds exercising prefix totality, FK integrity, clustering outcomes, agency synthesis, per-feed untouchedness, determinism and fail-soft. Its fixtures must stay byte-stable (files written in sorted order). RT bindings are gtfs-realtime-bindings/golang/gtfs v1.0.0 — no TripModifications support.