Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Build: control-core — Ids and the Domain

Maps to: Phase 0 (control-core). Kind: Build.

Objective

Stand up control-core, the crate that holds every type the rest of the control plane speaks, and fill in its two most foundational files: ids.rs (the RunId/JobId newtypes) and domain.rs (the status enums, the accounting structs, and the Run/Job aggregates). Prove with tests that ids are unique, serialize as bare UUID strings, and print as their inner UUID; that statuses serialize snake_case; that a JobOutcome totals its usage; and that a Job round-trips through JSON.

Scaffold

Create:

  • crates/control-core/Cargo.toml — the crate manifest. [dependencies]: serde (with derive), uuid (with v4, serde), chrono (with serde). [dev-dependencies]: serde_json (the round-trip and serialization tests encode to JSON), pretty_assertions (readable assert_eq! diffs).
  • crates/control-core/src/lib.rs — module declarations + re-exports (pub mod ids; pub mod domain; and pub use the types you want callers to reach without the module path).
  • crates/control-core/src/ids.rsRunId, JobId, and their tests.
  • crates/control-core/src/domain.rs — the domain types and their tests.

Why these deps: uuid gives you the 128-bit id and its v4 random constructor; the serde feature lets a Uuid serialize on its own so your transparent newtype has something to delegate to. chrono supplies DateTime<Utc> for Run::created_at, and its serde feature makes that field serialize as an RFC 3339 string.

Expected result: cargo test -p control-core6 tests pass:

  • from ids.rs: ids_are_unique, run_id_serializes_as_bare_uuid_string, display_matches_inner_uuid
  • from domain.rs: total_usage_sums_every_record, statuses_serialize_snake_case, job_roundtrips_through_json

The spec (givens)

ids.rs — the id newtypes. Two tuple structs, each wrapping a uuid::Uuid:

  • RunId(pub Uuid) and JobId(pub Uuid).
  • Derive stack on both: Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize.
  • Attribute on both: #[serde(transparent)] — the wire form is the bare quoted UUID, no wrapper.
  • Each gets an inherent new() -> Self returning a fresh Uuid::new_v4(), a Default impl that calls new(), and a Display impl that writes the inner UUID (write!(f, "{}", self.0)).

[→ Answer key](../appendix-answer-key.md#core-ids)

domain.rs — the vocabulary.

  • RunStatus: variants Queued, Running, Done, Failed. JobStatus: variants Pending, Assigned, Done, Failed. Both derive Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize and carry #[serde(rename_all = "snake_case")].
  • Usage: fields input_tokens: u32, output_tokens: u32. Derives Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize (the Default is what lets a fold start from zero).
  • Vignette: fields id: String, prompt: String. Derives Debug, Clone, PartialEq, Eq, Serialize, Deserialize.
  • EvalJob: fields vignettes: Vec<Vignette>, model: String, epoch: u32. Same derive stack as Vignette (no Copy — it owns a Vec).
  • ResponseRecord: fields vignette_id: String, model: String, epoch: u32, prompt: String, response: String, usage: Usage. Same derive stack.
  • Job: fields id: JobId, run_id: RunId, status: JobStatus, attempt: u32, spec: EvalJob. Same derive stack.
  • JobOutcome: fields job_id: JobId, records: Vec<ResponseRecord>. Same derive stack. Plus an inherent method total_usage(&self) -> Usage whose rule is: sum input_tokens and output_tokens across every record, independently, starting from Usage::default().
  • Run: fields id: RunId, status: RunStatus, created_at: DateTime<Utc>, manifest: String, models: Vec<String>, epochs: u32, job_count: u32, done_count: u32. Same derive stack as the other aggregates.

[→ Answer key](../appendix-answer-key.md#core-domain)

Concepts exercised

  • Newtype-over-Uuid for id safety; #[serde(transparent)] for bare-value serialization.
  • A hand-written Display that delegates to an inner field.
  • #[serde(rename_all = "snake_case")] on unit-variant enums.
  • Folding a Copy, Default accounting struct across a Vec.
  • Which aggregates can derive Copy (all fields Copy) and which cannot (owns a Vec or String).

The build loop (you drive)

Write each test first, predict its failure mode, run, then implement the minimum to green.

  1. ids_are_unique — asserts RunId::new() != RunId::new() and the same for JobId. Predict: does this fail to compile (the type does not exist yet) or fail an assertion? Run, confirm it is the former, then define the newtypes with new().
  2. run_id_serializes_as_bare_uuid_string — build a RunId, serde_json::to_string it, and assert the JSON equals format!("\"{}\"", id.0) (a bare quoted UUID, no [...] or {...}); then deserialize back and assert equality. This is the test that pins the wire contract — add #[serde(transparent)] to pass it and watch what removing the attribute does.
  3. display_matches_inner_uuid — assert id.to_string() == id.0.to_string(). Add the Display impl.
  4. statuses_serialize_snake_case — assert serde_json::to_string(&RunStatus::Queued) is "\"queued\"" and &JobStatus::Assigned is "\"assigned\"". Add the enums with rename_all.
  5. total_usage_sums_every_record — build a JobOutcome with two records (say 100/20 and 50/10 tokens) and assert total_usage() equals Usage { input_tokens: 150, output_tokens: 30 }. Implement the fold.
  6. job_roundtrips_through_json — build a full Job (with a nested EvalJob and a Vignette), encode to JSON, decode back, assert equal. This exercises every derive at once.
Predict first Before writing run_id_serializes_as_bare_uuid_string: with #[serde(transparent)] removed, what would serde_json::to_string(&RunId::new()) produce instead of a bare string — and would the assertion fail at the encode step or at the assert_eq!? Name it, then delete the attribute once and check.
NOTE Copy is safe on RunId, JobId, Usage, and the status enums because every field is itself Copy. It is not available on Vignette, EvalJob, ResponseRecord, Job, JobOutcome, or Run — they own a String or a Vec, which are moved, not copied. Deriving Copy on those will not compile, and the compiler will tell you exactly why.

Done when

cargo test -p control-core shows the six tests green, run_id_serializes_as_bare_uuid_string passes with #[serde(transparent)] present (and you have seen it fail with the attribute removed), and you can say which of the domain types derive Copy and why the rest cannot. Commit.