Skip to content

Latest commit

 

History

History
45 lines (30 loc) · 4.98 KB

File metadata and controls

45 lines (30 loc) · 4.98 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

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.

Commands

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.

Architecture (the parts that span multiple packages)

Two independent loops, connected only by an atomic snapshot pointer:

  • Static loop (internal/app/static_manager.gointernal/static): per-feed fetch timers → disk cache (internal/fetch) → on content-hash change, static.Build() runs scan → cluster → plan → stream-write → publishes an immutable static.Snapshot via atomic.Pointer. The snapshot holds the merged ZIP, per-feed ZIPs, cluster reports, and the model.Index (ID sets + stop→parent map).
  • RT loop (internal/app/rt_manager.gointernal/rt): one poller per (feed, URL). Pollers parse + prefix in place once, store into a Slot; 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.

Invariants — breaking these breaks consumers or tests

  • ID scheme is frozen: feedslug + ":" + originalID, applied to every ID column (internal/prefix holds the file→columns table) and every RT field (explicit field handling in internal/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). TestBuildDeterminism enforces this; never introduce map-iteration order or timestamps into anything that lands in the ZIP. feed_version is 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_id for feeds lacking one, e.g. carsud).
  • RT builder must never mutate slot messages — they are shared across rebuilds; it proto.Clones before Resolve() 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 after drop_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.Load uses strict YAML (KnownFields): new config keys require struct changes; durations/distances/sizes use the custom Duration/Meters/ByteSize types.

Testing notes

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.