Skip to content

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.

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 in clinker-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 raw PathBuf.

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-plan parses config, resolves schemas, compiles CXL, validates, and produces a typed ExecutionPlanDag; it does not depend on runtime operators. clinker-exec consumes 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.

// 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?

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.”