Capstone C1 — a synthetic extension
This is the first of two graduation capstones. You’ve read the engine end to end and extended each of its seams in guided lessons; C1 asks you to do it unguided, end to end, with tests, against a fixed rubric. It is synthetic on purpose: the seam you target doesn’t move between clinker revisions, so the task is stable and reviewable the same way every time, with no dependency on the live issue backlog (that’s C2).
What this proves (graduation outcomes 4–6, 8 + test discipline): that you can add a real extension at the right seam, wire every site the change touches, reason about which ownership/borrowing the seam requires, distinguish what’s validated at planning vs run time, and prove the whole thing with tests at the boundary.
What you’ll be able to do
Section titled “What you’ll be able to do”- Choose a real extension point in the engine and name the seam it plugs into: a CXL builtin’s two-table seam (add a CXL builtin) or a format’s trait + dispatch seam (add a reader/writer format).
- Wire every compiler-mandatory site the change touches, so the exhaustive matches you met earlier compile only once the change is complete.
- Write a test that exercises the new behavior through the public boundary (parse-to-eval for a builtin; read/round-trip for a format), not an internal helper in isolation.
- Distinguish a change that stays inside a seam from one that crosses an architectural boundary (a new dependency, a schema change, a
clinker-plan→clinker-execedge), and name which seam you stayed inside. - Run the full gauntlet (
fmt, both clippy passes, andcargo test --workspace) and read its result as the pass/fail signal it is.
New terms in this lesson (each is also expanded inline at the point you first need it):
- seam: the extension point a C1 change plugs into. Every earlier “add a …” lesson wired one.
- CXL: the engine’s expression language; Track A adds a method to it.
- DLQ: the dead-letter queue, re-tested below from the error-handling lesson.
Predict first
Section titled “Predict first”The task: pick one track
Section titled “The task: pick one track”Both tracks are real extension points with a clear analogue already in the source to read and copy. Track A is the lower-risk default; Track B is more end-to-end.
Track A (recommended): add a CXL builtin
Section titled “Track A (recommended): add a CXL builtin”Add a new scalar string method to CXL: title_case(), which upper-cases the first
letter of each whitespace-separated word ("hello world".title_case() → "Hello World").
It is genuinely new (it is not among the 24 existing string methods), so there is no
shortcut: you must wire both halves of the two-table seam from adding a CXL builtin.
The signature half is one entry in BuiltinRegistry::new() (no args, String receiver,
returns String):
cxl ·builtins.rs ·BuiltinRegistry type @19acdcb4
// in BuiltinRegistry::new(), the String-method array:s("title_case", vec![], 0, Some(0), TypeTag::String),The implementation half is one arm in dispatch_method (reuse a string_op helper):
cxl ·builtins_impl.rs ·dispatch_method fn @19acdcb4
"title_case" => Ok(Some(string_op(receiver, span, |s| { let cased = s .split_whitespace() .map(|w| { let mut c = w.chars(); match c.next() { Some(first) => first.to_uppercase().collect::<String>() + c.as_str(), None => String::new(), } }) .collect::<Vec<_>>() .join(" "); Value::String(cased.into())}))),Before you wire it into the two tables, you can verify the algorithm in isolation:
the same split_whitespace / upper-case-first-char logic, on a plain String, with no
clinker machinery. Run it; confirm it does what the spec says; then port the closure body
into the dispatch arm above.
> output appears here — press Run
The proof is a parse-to-eval test in the style of string_methods, driving a whole
program through parse → resolve → typecheck → eval:
cxl ·tests.rs ·string_methods test @19acdcb4
// assert that emit out = s.title_case() on "hello world"// evaluates to Value::String("Hello World")Track B: add a first-class format
Section titled “Track B: add a first-class format”Add a new FormatReader/FormatWriter and wire it through the config enum and the
exhaustive dispatch matches (adding a reader/writer format). The two required methods per trait:
clinker-format ·traits.rs ·FormatReader trait @19acdcb4
pub trait FormatReader: Send { fn schema(&mut self) -> Result<Arc<Schema>, FormatError>; fn next_record(&mut self) -> Result<Option<Record>, FormatError>;}The proof is a round-trip test (read → write → read, assert fields survive), like CSV’s:
clinker-format ·writer.rs ·test_csv_roundtrip_lossless test @19acdcb4
#[test]fn test_csv_roundtrip_lossless() { // read sample -> records, write -> bytes, read again -> assert equal}Acceptance criteria
Section titled “Acceptance criteria”A submission passes when all of these hold, on a branch in your own clinker checkout:
- Every site is wired. Track A: the registry entry and the dispatch arm. Track B:
the trait impl(s), the
InputFormat/OutputFormatvariant, theformat_namearm, and thebuild_format_reader/build_format_writerdispatch arm. The compiler-mandatory matches do not compile until these exist. - Tested at the boundary. A test that exercises the new behavior through the public path (parse-to-eval for a builtin; read/round-trip for a format), not an internal helper in isolation.
- The gauntlet is green.
fmt, both clippy passes, andcargo test --workspaceall pass (review & the gauntlet). - No boundary is crossed. No new dependency, no schema change, no
clinker-plan→clinker-execedge: the change stays inside the seam (planning a change safely). If you found yourself wanting any of those, that’s a C2/Decision-Gate situation, not C1.
Workflow
Section titled “Workflow”Work on a branch in the clinker checkout; clinker stays read-only as a source of truth and your branch is throwaway scratch:
git switch -c capstone/c1-title-case # or c1-<your-format># ...make the change...cargo test -p cxl --offline string_methods -- --exact # Track A: your new testulimit -n 4096 && cargo test --workspace --locked --offlinecargo fmt --all --checkcargo clippy --workspace --locked --offline -- -D warningscargo clippy --workspace --all-targets --locked --offline -- -D warningsThe rubric
Section titled “The rubric”Score your own submission (and have a reviewer score it) against this fixed rubric. “Meets” is the graduation bar; “Exceeds” is the stretch.
| Dimension | Below | Meets | Exceeds |
|---|---|---|---|
| Correctness | wrong behavior / tests fail | new behavior correct; targeted + workspace tests green | full gauntlet green incl. both clippy passes |
| Architectural fit | crosses a boundary unknowingly (new dep, schema, layering) | stays inside the seam; every dispatch site wired | names which seam it uses and why no boundary is touched |
| Tests & verification | no test, or tests an internal helper only | a test at the public boundary (parse-to-eval / round-trip) | adds an edge case or a property/oracle where apt |
| Review & conventions | ignores conventions | follows naming + test patterns of the existing analogue | a clean one-paragraph design note (what, where, why safe) |
// quick check
For Track B you decide to add a `tsv` format. The CSV reader already supports a tab delimiter. What does the rubric expect of you?
The architectural-fit dimension rewards recognizing when a change would add redundant surface. CSV's configurable delimiter already covers tab-separated input, so a bare tsv alias needs justification, exactly the plan-a-change-safely judgment. (Deleting a working, tested option to force your new path would itself be a boundary/behavior change.)
Retrieval checkpoint
Section titled “Retrieval checkpoint”When your synthetic extension is green and reviewed, you’re ready for the real thing: a genuine, potentially-mergeable contribution, scoped and planned the way the project actually works.
Go deeper on the Rust (optional, one-directional: the same concepts your C1 change leans on, taught from first principles in The Rust Book):
Glossary terms used: CXL, recoverable vs fatal, DLQ.