← Back to Blog
Building in Public

The Assembly Assumption

christian crumlish•
A robot on an assembly line with one connection missing, showing that assembled parts do not guarantee a working whole.
“Something’s not connected right”

February 14–20

The M0 sprint was supposed to take two to three weeks. We finished it in three days. We bragged about that in Weekly Ship #031, with a tiny bit of prescient hedging.

Five features. Over five hundred tests. A Lead Developer marathon that started before dawn on February 16 and didn’t stop until February 18. By Wednesday evening, every feature was complete, every test was green, and the sprint gate had evidence posted for all five capabilities.

I should have been celebrating. Instead, I was staring at a list of nine things that didn’t work.

The sprint that wasn’t supposed to be fast

M0 — “Conversational Glue” — was our most ambitious sprint yet. Five interconnected features that would transform Piper from a capable but mechanical assistant into something that felt more like a colleague:

  • Narrative onboarding: Recognizing when someone mentions their “main project” and responding naturally
  • Lens tracking: Following up on things mentioned earlier in conversation
  • Slot filling: Gathering information through dialogue rather than forms
  • Multi-intent handling: Decomposing complex requests into manageable pieces
  • Soft invocation: Recognizing implied needs and offering to help

Each feature had its own issue, its own gameplan, its own acceptance criteria. Each was implemented with rigorous test coverage. Each passed.

The sprint estimate of 13–22 days came from the scope. Five features, each touching multiple layers of the system, each requiring careful integration with existing capabilities. The three-day completion came from something else entirely — a Lead Developer session that ran twelve hours straight, methodically working through each feature with the kind of focus that only happens when everything else falls away.

By Wednesday evening, I had a complete sprint. Five hundred thirty-three new tests, all green. Sprint gate evidence posted. Ready for review.

The first hint

The trouble started almost immediately.

After closing the last feature (#767, soft invocation detection), the Lead Developer did what we always do: a quick sanity check to make sure everything hangs together. The check revealed something uncomfortable.

Each feature worked perfectly in isolation. But when you tried to use them together — the way an actual user would — they didn’t connect.

Soft invocation detection worked beautifully in its own tests. It recognized phrases like “I need to schedule that meeting” and correctly identified them as implied workflow needs. But the detected invocations never appeared in actual responses. The feature existed in a bubble, producing outputs that nothing consumed.

Lens tracking worked too. It could identify when a user referenced something from earlier in the conversation. But it wasn’t wired into the response pipeline. The lens was extracted, logged, and then… nothing.

The pattern repeated across all five features. Each one complete. Each one tested. Each one disconnected from the others.

Nine gaps

A deeper investigation revealed the full scope. Nine integration gaps, ranging from trivial (a missing two-line wire-up) to substantial (an entire subsystem with 124 tests and zero consumers):

The soft invocation detector produced offers that were never appended to responses. The lens extraction system tracked context that was never used. The offer accept/decline cycle had no way to complete — users could trigger an offer but couldn’t respond to it. The slot filling framework, with its elegant state machine and comprehensive test coverage, was entirely orphaned. Nothing in the system ever called it.

Some gaps were architectural. The trust stage was hardcoded to “BUILDING” because nobody had wired in the actual trust computation. Some were subtle. The lens stack had push and pop methods that were designed, documented, tested — and never invoked.

Each gap was small in isolation. A few lines here, a missing call there. But together they meant that M0, despite being “complete,” didn’t actually work.

The five whys

We use a technique called “five whys” to find root causes. You ask why something happened, then why that happened, and keep going until you hit something structural.

Why didn’t the features integrate? Because they were implemented as independent units.

Why were they implemented independently? Because each gameplan was scoped per-issue, with acceptance criteria that could be met in isolation.

Why were the gameplans scoped that way? Because the issues were written as independent capabilities, not user journeys.

Why were the issues written that way? Because the epic was decomposed into horizontal capability slices, not vertical user experience slices.

Why horizontal slices? Because that’s how we think about features. Each one a capability. Each one testable. Each one… separate.

The root cause wasn’t a bug. It wasn’t a missed requirement. It was an assumption baked into how we planned the work.

We assumed that individually correct components would compose into a correct system.

They didn’t.

This pattern has a name now: the Assembly Assumption. It’s the belief — usually implicit, rarely examined — that if you build each piece correctly, the whole will work correctly too.

The assumption feels reasonable. It’s how we think about most engineering. Build the engine, build the transmission, build the wheels, connect them together, and you have a car. Each component has a specification. Meet the specification, and composition follows.

But software doesn’t work that way. The connections between components aren’t physical interfaces with obvious shapes. They’re data flows, implicit contracts, shared state. Feature A produces something. Feature B consumes something. If A produces eight fields and B needs ten, both features “work” — A produces a valid output, B handles its inputs gracefully. The gap only appears when they’re composed.

Test coverage doesn’t catch this. Each feature’s tests verify the feature. The boundaries between features — the seams where data flows from one to another — exist in the spaces between specifications. They’re systematically excluded from what we verify.

The wiring pass

We fixed all nine gaps in a single session. Most were trivial once identified — the kind of thing where you look at the code and say “oh, obviously that needs to connect to that.” A few required more thought. One (the unified formality system) needed architectural review and got deferred.

But fixing the gaps wasn’t the real lesson. The lesson was that we needed a new step in our process.

After a sprint completes — after all the features are implemented and all the tests are green — we now do a “wiring pass.” It’s a dedicated session whose sole purpose is verifying composition. Not debugging (you don’t know what’s broken yet). Discovery (you’re finding what was never specified).

The wiring pass has a simple protocol:

Map the seams. Before touching code, draw the data flow across all features. Where does Feature A’s output become Feature B’s input? This map often reveals gaps before you look at any code.

Write integration scenarios. Not integration tests — scenarios. User journeys that cross feature boundaries. “User mentions their main project, then asks a multi-part question about it, and Piper responds with appropriate formality.” These scenarios are the specification for composition that was missing from the original planning.

Walk the code path. For each scenario, trace the actual execution through the composed system. Read the code. Verify that data flows correctly across boundaries. This is where implicit contract violations surface.

Fix and test. For each gap discovered, create a targeted integration test, then fix the gap. The test serves as the specification that was missing.

The wiring pass isn’t glamorous. It doesn’t produce new features or impressive test counts. But it’s the difference between a sprint that’s complete and a sprint that works.

The humbling part

I’ve been making software for a long time. I’ve written about methodology, about verification, about the importance of systematic approaches. I should have seen this coming.

But I didn’t. I watched a three-day sprint produce five hundred tests and felt satisfied. The tests were green. The features were complete. The methodology was working.

Except it wasn’t working. It was verifying slices while missing the whole.

The Assembly Assumption is particularly insidious because it hides behind good practices. Test-driven development? We did that. Comprehensive coverage? We had it. Rigorous acceptance criteria? Every feature met them. The assumption doesn’t contradict good practice — it lives in the gaps between good practices.

Agile methodologies actually make this worse. The emphasis on small, independent stories encourages horizontal decomposition. Each story is a unit. Each unit is testable. The planning framework itself creates the blind spot.

I don’t have a perfect solution. The wiring pass helps, but it’s reactive — you still build the features separately and then discover the gaps. A better approach would catch composition issues during planning, before implementation begins. We’re working on that.

For now, the wiring pass is a checkpoint. A reminder that individually correct components don’t guarantee a correct composition. A scheduled moment to ask: does this actually work together?

What remained

By the end of February 18, M0 was actually complete. Not just tested-and-green complete, but wired-together-and-working complete. The wiring pass added 79 more tests — integration tests that verified the seams we’d been missing.

The sprint that was supposed to take three weeks finished in three days, plus one more day to discover that “finished” wasn’t the same as “working.”

It was a good reminder. The methodology isn’t done evolving. The patterns we trust can have blind spots. The assumptions we don’t examine are the ones that bite us.

The Assembly Assumption is documented now, as Pattern-062 in our pattern library. It joins a growing collection of things we’ve learned the hard way — not from theory, but from building something and watching it fail to compose.

Next time, we’ll plan differently. We’ll think about vertical slices, not just horizontal capabilities. We’ll schedule the wiring pass before the sprint gate, not after.

And we’ll remember: individually correct is not the same as collectively working.

← Back to BlogAlso available on Medium