Plan a change without breaking a boundary
You can now extend all three of the engine’s seams. The last skill is the one that separates a contributor from a careful contributor: knowing before you write code which architectural boundary your change touches, and recognizing the decisions that aren’t yours to make alone. This is the capstone lesson — and the bridge to the capstones themselves.
You’ll be able to: read the engine’s architectural invariants, use the crate-layering rule to predict whether a change is safe, and decide when a change needs a Decision Gate before any code is written.
The invariants are written down
Section titled “The invariants are written down”Clinker keeps its architectural rules explicit, in docs/ai. They’re not style
preferences — they’re the load-bearing properties everything else assumes. A few of the
most concrete (quoted):
clinker ·10_ARCHITECTURE.md ·Pipelines compile before they execute. doc @47d2e12
Pipelines compile before they execute. Planning/config validation belongs in
clinker-plan; runtime operator execution belongs inclinker-exec.
That single rule is the one you’ve now seen from four angles: the typed CompiledPlan
handle (3.5), the lowering step (5.4), and the plan-vs-exec crate split (5.4 again). Other
invariants in the same register:
- The runtime is finite and synchronous — “Do not introduce unbounded streams, daemon/service loops, distributed execution, or async-runtime assumptions without architecture review.”
- Bounded memory is load-bearing — the arbitrator’s 512 MiB default and the spill machinery (Phase 4) are a contract, not an implementation detail.
- One YAML chokepoint — all YAML parsing goes through
clinker_plan::yaml, the only place that calls the underlying parser, where input limits are enforced. - Path trust uses proof tokens — APIs that need a trusted path take a
ValidatedPath(lesson 3.3), not a rawPathBuf.
A change that quietly violates one of these isn’t a bug in the usual sense — it’s a boundary break that the type system might not catch. Which is why you check the invariants first.
The crate layering is the boundary you’ll meet most
Section titled “The crate layering is the boundary you’ll meet most”The most frequently-touched boundary is the dependency direction between crates. Dependencies flow one way — “from lower-level vocabulary toward applications”:
clinker ·20_CRATE_MAP.md ·from lower-level vocabulary toward applications doc @47d2e12
clinker-core-types (leaf vocabulary — no exec/config/schema types)clinker-record → cxl → clinker-format → clinker-plan (parses, validates, lowers → CompiledPlan) → clinker-exec (consumes CompiledPlan, runs operators) → clinker-net → clinker / cxl-cli (the application edges)The rule that polices it:
clinker ·30_DESIGN_RULES.md ·Preserve the current crate layering. doc @47d2e12
Preserve the current crate layering.
clinker-planparses config, resolves schemas, compiles CXL, validates, and produces a typedExecutionPlanDag; it does not depend on runtime operators.clinker-execconsumes compiled plans and owns runtime dispatch.
You can prove this boundary holds by reading a manifest. clinker-plan’s dependencies
list its lower layers — and pointedly not clinker-exec:
clinker-plan ·Cargo.toml ·clinker-format doc @47d2e12
[dependencies]clinker-core-types = { workspace = true }clinker-record = { workspace = true }cxl = { workspace = true }clinker-format = { workspace = true }# clinker-exec is absent — plan must not depend on exec.This is why the operator change in lesson 5.4 split the way it did: sites 1–3 (config,
plan, lowering) live in clinker-plan; the dispatch arm and operator body live in
clinker-exec, which depends on clinker-plan — never the reverse. If your change tempted
you to make clinker-plan call into a runtime operator, you’d have to add clinker-exec
to that manifest, creating a cycle — and that’s the exact boundary the rule forbids. The
manifest is the boundary made mechanical.
Some decisions aren’t yours to make: the Decision Gate
Section titled “Some decisions aren’t yours to make: the Decision Gate”The hardest part of planning a change is recognizing when you’ve hit a question you shouldn’t answer unilaterally. Clinker’s workflow names this explicitly: a Decision Gate is opened — and resolved — before implementation, whenever a change depends on an unresolved choice.
clinker ·DECISIONS.md ·When To Create A Decision Gate doc @47d2e12
The gate triggers (quoted) for:
- new dependency or cargo-deny exception
- public API behavior
- data model, schema, storage, or migration behavior
- auth, security, privacy, or credentials
- performance or bounded-memory tradeoff with unclear priority
- backward compatibility or breaking changes
- cross-crate or cross-service architecture boundaries
- any behavior the agent cannot validate from existing docs, tests, or source
That last bullet is the catch-all and the most important: if you can’t ground the right answer in existing source, tests, or docs, stop and gate it — don’t guess and encode the guess. Resolving a gate means recording the chosen option, the rejected options, the source evidence, and the consequences. And if the question can’t be resolved, it’s written down rather than silently decided:
clinker ·80_OPEN_QUESTIONS.md ·Open Questions doc @47d2e12
The open-questions register is where unresolved boundary questions live — things like “what
is the intended boundary between clinker-schema and clinker-plan?” A contributor’s job
is to recognize their change is about to answer one of these, and route it to a gate
instead of deciding it in a pull request.
Reading a change against the boundaries
Section titled “Reading a change against the boundaries”// quick check
Your change makes the aggregation operator faster by having clinker-plan precompute a lookup the operator reads. To do it cleanly you'd add `clinker-exec` to clinker-plan's dependencies. What's the right move?
'clinker-plan does not depend on runtime operators' is an explicit invariant, and the manifest enforces the one-way arrow. Adding clinker-exec to clinker-plan creates a cycle and breaks the plan/runtime boundary. Cross-crate architecture changes are an explicit Decision Gate trigger — escalate, don't encode the guess. (Copying the code just hides the same coupling.)
Plan one
Section titled “Plan one”Where this leaves you — the capstones
Section titled “Where this leaves you — the capstones”You’ve read the engine end to end, extended each of its seams, and learned to land a change without breaking a boundary. Two capstones put it together:
- C1 — synthetic: add a small extension end-to-end with tests — a new format
(
FormatReader/FormatWriter, lesson 5.3) or a CXL builtin (lesson 5.2). It’s stable across revisions and reviewable against a fixed rubric — the safe seams you now know. - C2 — real: a genuine, potentially-mergeable contribution selected fresh from the live backlog through the project’s Readiness-Review / Decision-Gate workflow — scoping, review conventions, and architectural reasoning under supervision.
That’s the whole arc of the Engine Track: from “build and run Clinker” to “make a real, boundary-respecting contribution to it.”