The Skeleton Is the Spec
A prose spec constrains the agent weakly. A skeleton of types and signatures constrains it mechanically:
/// Decodes raw frames into engineering-unit signal values.
pub struct SignalDecoder { /* fields to be decided */ }
impl SignalDecoder {
/// Parse a decoder from a signal database. Rejects malformed input.
pub fn from_dbc(src: &str) -> Result<Self, DbcParseError> { todo!() }
/// Decode one frame. A frame for an unknown message is not an error.
pub fn decode(&self, frame: &Frame) -> Option<DecodedSignals> { todo!() }
}
These nine lines already rule out several kinds of generated code:
- A decoder built from a partially parsed database.
Resultforces the question of what happens on bad input. - Treating an unknown frame as an error or a crash.
Optiondeclares it a normal outcome. - Mutating the decoder while decoding.
&selfforbids it, which also keeps decoding shareable across threads. - An improvised error strategy.
DbcParseErroris a type you defined.
The workflow is a loop, and the division of labor in it doesn’t change: bodies are generated, skeletons are written by hand. A skeleton is a page of code at most, so typing it’s the cheap part. The decisions it records are the work, and a generated skeleton is a set of decisions nobody made.
- You write the skeleton.
- The agent fills in the bodies.
- You review the bodies.
- When a review finding recurs, you change the skeleton so that the whole class of defect no longer compiles, and regenerate.
Step 4, concretely: a review that keeps finding unwrap on lookups
changes the signature the bodies are generated against.
// before: bodies keep unwrapping the miss
pub fn scale_for(&self, id: CanId) -> f64 { todo!() }
// after: the miss is in the type; unwrapping it is now visible
pub fn scale_for(&self, id: CanId) -> Option<f64> { todo!() }