Rust workspace · finished architecture

Panoptes ETL: types & data flow

The space-data ETL that feeds the Panoptes harness — the complete four-crate project the sequel course builds. Four sources normalize into one Row enum written as JSONL; a DAG executor schedules the pipelines; a retrieval arc embeds the prose. Every arc installs one seam an orchestrator needs.

etl-sources etl-orchestrate panoptes-etl all → etl-core etl-orchestrate depends on core only; it schedules dyn Source and never names a concrete source.

The story, end to end

Everything hangs off one trio of traits. A Source fetches raw bytes — extract returns a RawRecord (the payload, plus where and when it came from); a Transform parses those bytes into normalized Rows; a Sink writes them. Row is a single enum — a SpaceObject (a satellite and its OrbitalElements), a Conjunction, or a Launch — so one sink accepts the output of any source without knowing which produced it. Each real source is just a Source + Transform pair: CelestrakSource fetches TLE text and parses its columns, checksum, and epoch; SpaceTrackSource logs in for a session cookie and pulls conjunction data messages, politely throttled by a hand-rolled TokenBucket; SpaceDevsSource walks a paginated API. The keystone is the Conjunction — a close approach between two tracked objects, which is a collision-avoidance decision waiting to be posed to a model, and the ground truth a Panoptes ca_geo vignette is built from.

What makes this an ETL and not a pile of scripts is four seams. EtlError splits every failure into transient (a dropped connection, a 429) or permanent (a 404, malformed data), and with_retry backs off and retries only the transient ones. NoradId is a newtype, so a catalog number can never be mistaken for any other integer. The Ledger is an append-only record of every run — which source, when, the high-water watermark it reached, success or failure — and it is what lets a re-run skip a source that is still fresh. content_id hashes a row's content into a stable id, so IdempotentSink recognizes a row it has already written and a backfill costs nothing. None of these is clever on its own; together they are the joints an orchestrator needs.

The payoff is the Executor. You describe the work as a Dag — a graph of tasks — and it runs them in topological order, retrying transient failures, gating a node when its upstream fails, and skipping sources the ledger says are current. Crucially, etl-orchestrate depends on etl-core alone: it schedules a dyn Source and cannot even name a concrete source, which is why adding the NASA capstone changes nothing about the executor. Last is retrieval: EmbedSink runs the prose fields — a launch's description, never the numbers — through an Embedder (the VoyageEmbedder), content-addressed so any text is embedded once, and a VectorIndex answers top_k by cosine similarity. The normalized JSONL and that vector index are the two artifacts Panoptes reads.

01 The pipeline

Each source extracts raw bytes, a transform parses them into typed Rows, and an idempotent sink appends JSONL that panoptes-gen reads. The Executor runs the pipelines in dependency order — retrying transient failures, gating on upstream failure, skipping sources still fresh — and the retrieval arc embeds the prose fields into a vector index.

wrapsdrivesdrivesdrivesrecords

panoptes-etl CLI · run · backfill · embed · status

CelesTrak · open TLE GET

CelestrakSource

Space-Track · login + cookie

SpaceTrackSource

TheSpaceDevs · paginated

SpaceDevsSource

TokenBucket · rate limit

with_retry · backoff on transient EtlError

RawRecord

Transform · parse TLE / CDM / launch JSON

Row · SpaceObject · Conjunction · Launch

IdempotentSink · content_id() dedupe

space_objects · conjunctions · launches · .jsonl

Executor · Dag topological order · gate on failure · skip fresh

Ledger · RunRecord · watermark

EmbedSink · Embedder → VoyageEmbedder · prose only

embeddings.jsonl

VectorIndex::top_k() · cosine

panoptes-gen reads the file contract

teal = etl-core · slate = etl-sources · amber = etl-orchestrate · plum = panoptes-etl · cylinders = files · dashed = external API / downstream

02 Type relationships

The E/T/L trait triad — Source, Transform, Sink — is the spine. Concrete sources implement Source; the domain types are the payload of the Row enum the sink writes; and Pipeline/Executor compose the traits as boxed trait objects, which is what lets the orchestrator stay ignorant of any concrete source.

etl_core

etl_sources

etl_orchestrate

«newtype»

NoradId

+u32 inner

«enum»

OrbitClass

LEO

MEO

GEO

HEO

OrbitalElements

+DateTime epoch

+f64 inclination_deg

+f64 eccentricity

+f64 mean_motion_rev_per_day

+orbit_class() : OrbitClass

+period_minutes() : f64

SpaceObject

+NoradId norad_id

+String name

+String intl_designator

+OrbitalElements elements

Conjunction

+String id

+DateTime tca

+f64 miss_distance_km

+f64 collision_probability

+NoradId primary

+NoradId secondary

Launch

+String id

+String name

+DateTime net

+String provider

+String description

«enum»

Row

SpaceObject

Conjunction

Launch

«enum»

EtlError

Network

RateLimited

Auth

Permanent

Parse

Io

RawRecord

+String source

+String payload

+DateTime fetched_at

«trait»

Source

+name() : str

+extract() : Vec<RawRecord>

«trait»

Transform

+transform(RawRecord) : Vec<Row>

«trait»

Sink

+load(rows) : usize

Ledger

+append(RunRecord)

+last_success(source) : RunRecord

RunRecord

+String source

+Outcome outcome

+DateTime watermark

+usize rows_written

«enum»

Outcome

Success

Failed

«trait»

Embedder

+embed(texts) : vectors

VectorIndex

+top_k(query, k) : hits

CelestrakSource

SpaceTrackSource

SpaceDevsSource

TokenBucket

VoyageEmbedder

Dag

+add_task(id) : TaskId

+add_dep(task, dep)

+topological_order() : Vec<TaskId>

Pipeline

+Box<Source> source

+Box<Transform> transform

+Box<Sink> sink

Executor

+run() : RunReport

◆── composition · ○── aggregation (boxed trait object) · ┈▷ implements · «enum» / «trait» / «newtype» stereotypes · ~T~ = generic

03 Per-crate inventory

One seam per arc; the dependency arrow always points at etl-core.

etl-core

The seams. Domain model, the E/T/L traits, and everything the orchestrator schedules against.

  • enumOrbitClass · EtlError · Row · Outcome
  • newtyNoradId · id-safety
  • structSpaceObject · OrbitalElements
  • structConjunction (keystone) · Launch
  • traitSource · Transform · Sink
  • structLedger · JsonlSink · IdempotentSink
  • fnwith_retry · content_id
  • traitEmbedder · VectorIndex · cosine

etl-sources

Concrete sources + the machinery each API forces. Depends only on core.

  • fnparse_tle · checksum, epoch
  • structCelestrakSource (+ transform)
  • structSpaceTrackSource · Credentials
  • structSpaceDevsSource (pagination)
  • structTokenBucket · rate limit
  • fnclassify_status · taxonomy funnel
  • structVoyageEmbedder

etl-orchestrate

The DAG executor. Depends on core ONLY — proven by cargo tree.

  • structDag · TaskId
  • fntopological_order · Kahn, cycle-safe
  • structPipeline · boxed E/T/L
  • structExecutor · RunReport

panoptes-etl

The binary. The only crate that names concrete sources and wires them into the DAG.

  • binCli · Command
  • cmdrun · backfill · status
  • cmdembed (prose → vectors)
  • fnbuild_executor (wiring)
  • noteNASA source = your capstone

04 Reading the diagrams

  • ◆──Composition — the struct owns the value (every SpaceObject owns its OrbitalElements).
  • ○──Aggregation — holds a boxed trait object (Pipeline holds a Box<dyn Source>).
  • ┈▷Trait implementation — three sources implement Source; VoyageEmbedder ▷ Embedder.
  • «enum»A closed set — the transform parses raw bytes straight into these, so bad data fails at the boundary.
  • Cylinders are JSONL files on disk; dashed nodes are external APIs or the downstream panoptes-gen.
  • colourcore · sources · orchestrate · bin.