Part II: Discovery Through Software Engineering and Vibe Coding
Chapter 7: Software Development As A Discovery Process

7.1 SDLC as Iterative Hypothesis Testing

"We wrote a 200-page requirements document. Six months later, we discovered the users wanted something completely different. If only we had tested the hypothesis before writing the specification."

A Waterfall Project Manager Seeking Redemption

Prerequisites

This section builds directly on the search framework \((S, A, T, f, C)\) introduced in Section 1.1 and the hypothesis testing paradigm from Chapter 2. We also reference the regret analysis from Section 1.2. Familiarity with basic software engineering concepts (requirements, testing, deployment) is assumed; no specific methodology experience is needed.

The Big Picture

The traditional view of software development treats it as an engineering discipline: gather requirements, design, implement, test, deploy. This linear model assumes that requirements are knowable in advance and that the primary challenge is faithful translation from specification to code. In practice, many software projects fail not because of poor implementation but because of incorrect assumptions about what to build. This section reframes the software development life cycle (SDLC) as a discovery process where every phase generates and tests hypotheses, where uncertainty is the default state, and where the goal is not to follow a plan but to learn as quickly as possible what the right product is.

1. The Manufacturing Illusion

In 2005, the FBI scrapped the Virtual Case File project after spending over \$170 million, having discovered, years into development, that field agents needed something fundamentally different from what the specification described. Software engineering inherited its vocabulary from manufacturing. We talk about "building" software, "shipping" products, and running a "factory" of sprints. But nobody manufactures software. A developer writes each line of code exactly once (or should). No assembly line produces identical copies. Writing software means discovering what the program should be, one decision at a time.

Consider a concrete example. A team is asked to build a recommendation engine for a scientific paper discovery platform. At the start, they face cascading uncertainties:

Each of these uncertainties is a hypothesis waiting to be tested. The waterfall approach treats them as questions to be answered in sequence before writing code. The discovery approach treats them as experiments to be run as quickly as possible, with code as the experimental apparatus.

2. Formalizing SDLC as Search

When teams skip this formal mapping, they default to intuition about what to build next, and intuition scales poorly: the Standish Group's CHAOS reports consistently find that fewer than 35% of software projects succeed, with "incomplete requirements" and "lack of user involvement" topping every failure list. Making the search structure explicit is what turns those odds around.

The software development process maps directly onto the search framework from Section 1.1. The five-tuple \((S, A, T, f, C)\) for software discovery is:

The key insight is that \(f\) is unknown at the start. The team does not fully know what a good outcome looks like until they have explored enough of the space to understand user needs. This makes software development a problem of search under uncertainty, precisely the setting where exploration strategies from Section 1.2 apply. In short: Software development is not a manufacturing problem with a known blueprint; it is a search problem with an unknown objective.

Key Insight: Requirements Are Hypotheses, Not Facts

Every requirement in a software project is a hypothesis of the form: "We believe that [user group] needs [capability] because [reason], and we will know we are right when [measurable outcome]." Treating requirements as hypotheses changes everything about how we prioritize, implement, and validate them. A hypothesis can be tested with a prototype, a mockup, or even a conversation. A "requirement" implies certainty and demands a complete implementation. The hypothesis framing tends to be more honest and more productive.

Mental Model

Mapping software development onto the search tuple \((S, A, T, f, C)\) with an unknown objective \(f\) is like navigating a foreign city without a destination address. You have a map of the streets (the state space \(S\)), you can walk, take a bus, or hail a taxi (actions \(A\)), and each move lands you somewhere new (transition \(T\)). But nobody told you where you are going; you only discover the destination by wandering, talking to locals, and noticing which neighborhoods feel right. Budget and closing times constrain your wandering (\(C\)). The crucial parallel is that you cannot simply plan the shortest route first and then walk it, because the endpoint itself is something you learn through exploration. A detailed street-by-street itinerary written before you start (the waterfall specification) is wasted effort if, three turns in, a local tells you the real landmark is across town.

Common Misconception

A frequent misreading of "requirements are hypotheses" is that hypothesis-driven development means the team should never commit to a direction or that every decision must be validated by a formal experiment before proceeding. This is incorrect: the hypothesis framing does not eliminate commitment, it makes commitment incremental and evidence-based. Teams still make firm decisions and ship features; the difference is that each commitment is sized to match the current level of evidence, starting with cheap tests (mockups, user conversations, spike prototypes) and scaling up investment only as confidence grows.

3. The Four Phases as Hypothesis Operations

Every SDLC methodology, regardless of its specific practices, cycles through four fundamental operations on hypotheses. As Figure 7.1 illustrates, these four operations form a continuous loop, with each cycle's output feeding the next round of generation. We can formalize these using the language of Chapter 2:

Generation Prioritization Testing Updating Backlog grooming, ideation Explore vs. exploit trade-off Experiments, A/B tests, demos Bayesian belief revision select next run experiment gather evidence revise hypotheses
Figure 7.1: The four hypothesis operations in the SDLC discovery cycle. Each phase feeds the next: Generation produces candidate hypotheses, Prioritization selects which to test next, Testing gathers evidence, and Updating revises beliefs, which triggers a new round of generation.

Generation: producing candidate hypotheses about what to build and how. In agile, this happens during backlog grooming and sprint planning. In waterfall, it happens during requirements gathering. The quality of generation determines the breadth of the search.

Prioritization: selecting which hypotheses to test next. This is the exploration/exploitation trade-off from Section 1.2. Greedy prioritization (always build the feature with the highest expected value) exploits current knowledge but may miss better options. Exploratory prioritization (build the feature with the highest uncertainty) gathers information but delays value delivery.

Mental Model

Think of prioritization like choosing which dish to order at a new restaurant. The greedy strategy is ordering the most popular item every time: safe, but you may never discover your actual favorite. The exploratory strategy is trying something unfamiliar each visit: you waste a few meals on dishes you dislike, but you eventually find the one you love. The optimal approach (analogous to Upper Confidence Bound (UCB) or Thompson Sampling, where the team samples each feature's value from a probability distribution and picks the highest sample rather than the highest point estimate) orders unfamiliar dishes when you have eaten at the restaurant only a few times and gradually shifts toward your proven favorites as your knowledge of the menu grows. The key parallel is that the cost of a bad order (one disappointing meal) is small compared to the cost of never finding the best dish (a lifetime of "good enough"), just as the cost of one exploratory sprint is small compared to months spent building the wrong feature.

Testing: running experiments to evaluate hypotheses. In software, experiments take many forms: unit tests, integration tests, A/B tests (controlled experiments that split users into groups to compare two variants of a feature), user interviews, prototype demos, canary deployments (where a new release is rolled out to a small subset of production servers before reaching all users), and production monitoring. Each reduces uncertainty about one or more hypotheses.

Updating: revising beliefs and plans based on experimental results. In Bayesian terms, we update our posterior distribution, the revised probability distribution over possible outcomes after observing new evidence, over the space of "good products" given the evidence gathered. In agile, this happens during retrospectives and backlog re-prioritization. Figure 7.1.1 illustrates SDLC hypothesis cycle with discovery velocity feedback loop.

SDLC hypothesis cycle with discovery velocity feedback loop
Figure 7.1.1: The four-phase hypothesis cycle (Generate, Prioritize, Test, Update) with discovery velocity as the central feedback metric, mapped to concrete SDLC activities.

The following formalization captures this cycle. Define a hypothesis \(h_i\) as a tuple \((claim_i, test_i, evidence_i, confidence_i)\) where \(claim_i\) is a falsifiable (that is, stated precisely enough to be proven wrong by a specific observation) statement about what to build, \(test_i\) is a procedure to evaluate the claim, \(evidence_i\) collects observations from running the test, and \(confidence_i \in [0, 1]\) is the team's posterior belief that the claim is correct.

from dataclasses import dataclass, field
from typing import Callable, Optional
import json
from datetime import datetime


@dataclass
class DevHypothesis:
    """A falsifiable hypothesis in a software development process.

    Each hypothesis represents an uncertain belief about what to build,
    how to build it, or whether it will deliver value. The confidence
    field tracks the team's evolving belief as evidence accumulates.
    """
    claim: str                           # Falsifiable statement
    category: str                        # "requirement", "architecture", "implementation", "value"
    test_procedure: str                  # How to test the claim
    success_criterion: str               # What counts as confirmation
    confidence: float = 0.5             # Prior belief, updated with evidence
    evidence: list = field(default_factory=list)
    status: str = "untested"            # "untested", "testing", "confirmed", "refuted"
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())

    def update_confidence(self, observation: str, confirms: bool,
                          strength: float = 0.1) -> float:
        """Bayesian-style confidence update given new evidence.

        Uses a simplified update rule: confidence moves toward 1.0 if
        the observation confirms the hypothesis, toward 0.0 if it refutes.
        The strength parameter controls the magnitude of the update.
        """
        if confirms:
            self.confidence = self.confidence + strength * (1.0 - self.confidence)
        else:
            self.confidence = self.confidence - strength * self.confidence

        self.evidence.append({
            "observation": observation,
            "confirms": confirms,
            "strength": strength,
            "timestamp": datetime.now().isoformat(),
            "confidence_after": round(self.confidence, 4)
        })

        # Update status based on accumulated evidence
        if self.confidence > 0.9:
            self.status = "confirmed"
        elif self.confidence < 0.1:
            self.status = "refuted"
        else:
            self.status = "testing"

        return self.confidence

    def to_user_story(self) -> str:
        """Convert a confirmed requirement hypothesis to a user story."""
        if self.category != "requirement":
            raise ValueError("Only requirement hypotheses become user stories")
        return f"As a user, I want {self.claim} so that {self.success_criterion}"


# Example: hypotheses for a paper recommendation system
hypotheses = [
    DevHypothesis(
        claim="Researchers prefer citation-based recommendations over content similarity",
        category="requirement",
        test_procedure="A/B test with 50 users comparing citation-graph vs. embedding similarity",
        success_criterion="Citation-based click-through rate (CTR) exceeds content-based CTR by >10%",
        confidence=0.6  # Slight prior toward citation-based
    ),
    DevHypothesis(
        claim="SPECTER2 embeddings outperform TF-IDF for scientific paper similarity",
        category="implementation",
        test_procedure="Offline evaluation on RELISH benchmark dataset",
        success_criterion="SPECTER2 achieves >0.85 normalized Discounted Cumulative Gain (nDCG) @10 vs TF-IDF baseline",
        confidence=0.7  # Strong prior based on published results
    ),
    DevHypothesis(
        claim="Users will engage with recommendations if latency is under 200ms",
        category="value",
        test_procedure="Deploy with latency monitoring and track engagement rate",
        success_criterion="Engagement rate >15% when p95 latency < 200ms",
        confidence=0.5  # Genuine uncertainty
    ),
]

# Simulate evidence arriving from experiments
hypotheses[0].update_confidence("Pilot users clicked 2x more on citation recs", True, 0.2)
hypotheses[0].update_confidence("Power users preferred content recs for cross-field", False, 0.15)

for h in hypotheses:
    print(f"[{h.status:>10}] {h.confidence:.2f}  {h.claim[:60]}...")
Listing 7.1: DevHypothesis dataclass tracking falsifiable claims through the evidence lifecycle, with Bayesian confidence updates and status transitions.

Exercise 7.1.1

A team starts with three hypotheses, each at confidence 0.5. During Sprint 1, they test hypothesis A and receive a confirming observation with strength 0.3. During Sprint 2, they test hypothesis B and receive a refuting observation with strength 0.2. Hypothesis C remains untested. Compute the total entropy before any testing and after both sprints. By how many bits did the team's uncertainty decrease? What is their discovery velocity in bits per sprint?

Hint

Binary entropy for a single hypothesis is \(-(c \log_2 c + (1-c) \log_2(1-c))\). At \(c=0.5\) the entropy is 1.0 bit. After a confirming update with strength 0.3, the new confidence is \(0.5 + 0.3 \times (1 - 0.5) = 0.65\). After a refuting update with strength 0.2, the new confidence is \(0.5 - 0.2 \times 0.5 = 0.4\). Compute the binary entropy at each new confidence and sum across all three hypotheses.

4. Discovery Velocity

If software development is discovery, we need a metric for how fast a team is discovering. Lines of code per day and story points per sprint measure output, not learning. A team that ships ten features nobody wants has high output velocity but zero discovery velocity.

We define discovery velocity \(v_d\) as the rate of uncertainty reduction per unit time:

Discovery velocity measures how quickly a team converts unknowns into validated decisions, expressed in bits of resolved uncertainty per unit time. It matters because traditional productivity metrics (lines of code, features shipped) reward output regardless of whether that output addresses the right problem, while discovery velocity directly captures the team's rate of learning. The mechanism is straightforward: each tested hypothesis either confirms or refutes a belief, sharpening the team's probability distribution over possible products and reducing its Shannon entropy, the information-theoretic measure of uncertainty in a probability distribution (Shannon, 1948). Use discovery velocity when the primary risk is building the wrong thing (most new products and features); fall back to throughput metrics only for well-understood maintenance work where requirements are stable and the search space is already narrow.

$$v_d = \frac{\Delta H}{t} = \frac{H(S_{t_0}) - H(S_{t_1})}{t_1 - t_0}$$

where \(H(S_t)\) is the entropy of the team's belief distribution over "what is the right product to build" at time \(t\). When \(H\) is high, the team is uncertain; as hypotheses are tested and beliefs sharpen, \(H\) decreases. A team with high discovery velocity resolves uncertainty quickly, converging on the right product faster.

In practice, we approximate \(H\) using the set of active hypotheses:

$$H(S_t) \approx -\sum_{i=1}^{n} \left[ c_i \log_2 c_i + (1 - c_i) \log_2(1 - c_i) \right]$$

where \(c_i\) is the confidence in hypothesis \(i\) and \(n\) is the number of active hypotheses. Note that the logarithm is base 2 so that entropy is measured in bits, matching the code in Listing 7.2. Each term reaches its maximum of 1 bit when \(c_i = 0.5\) (maximum uncertainty) and drops to zero when \(c_i\) approaches 0 or 1 (resolved).

import numpy as np
from typing import List


def compute_entropy(hypotheses: List[DevHypothesis]) -> float:
    """Compute the total uncertainty across all active hypotheses.

    Uses binary entropy for each hypothesis: maximum at confidence = 0.5,
    zero at confidence = 0 or 1. The sum gives total team uncertainty.
    """
    total = 0.0
    for h in hypotheses:
        c = np.clip(h.confidence, 1e-10, 1 - 1e-10)  # Avoid log(0)
        total += -(c * np.log2(c) + (1 - c) * np.log2(1 - c))
    return total


def discovery_velocity(hypotheses: List[DevHypothesis],
                       entropy_before: float,
                       time_elapsed_days: float) -> float:
    """Compute discovery velocity as bits of uncertainty resolved per day."""
    entropy_after = compute_entropy(hypotheses)
    return (entropy_before - entropy_after) / max(time_elapsed_days, 1e-6)


# Measure discovery velocity for our recommendation system team
entropy_t0 = compute_entropy(hypotheses)
print(f"Initial entropy: {entropy_t0:.3f} bits")

# After one sprint (2 weeks), some hypotheses have been tested
hypotheses[1].update_confidence("SPECTER2 nDCG@10 = 0.91 on RELISH", True, 0.25)
hypotheses[2].update_confidence("Prototype p95 latency = 150ms, engagement = 18%", True, 0.3)

entropy_t1 = compute_entropy(hypotheses)
v_d = discovery_velocity(hypotheses, entropy_t0, time_elapsed_days=14)
print(f"Entropy after sprint: {entropy_t1:.3f} bits")
print(f"Discovery velocity: {v_d:.4f} bits/day")
Listing 7.2: Entropy computation and discovery velocity measurement across a hypothesis portfolio, using binary entropy per hypothesis.

Step-Through: Bayesian Confidence Update

Trace through two rounds of update_confidence for a hypothesis starting at confidence \(c_0 = 0.5\).

Round 1 (confirming observation, strength 0.2):
\(c_1 = 0.5 + 0.2 \times (1.0 - 0.5) = 0.5 + 0.1 = 0.6\)
Binary entropy: \(-(0.6 \log_2 0.6 + 0.4 \log_2 0.4) = -(0.6 \times {-0.737}) - (0.4 \times {-1.322}) = 0.442 + 0.529 = 0.971\) bits.

Round 2 (refuting observation, strength 0.15):
\(c_2 = 0.6 - 0.15 \times 0.6 = 0.6 - 0.09 = 0.51\)
Binary entropy: \(-(0.51 \log_2 0.51 + 0.49 \log_2 0.49) \approx 0.9997\) bits.

Notice that the refuting evidence pushed confidence back near 0.5, almost restoring maximum uncertainty. Two observations of opposite sign largely cancel, leaving the hypothesis nearly as unresolved as before. This illustrates why consistent evidence in one direction is needed to drive entropy down and why contradictory signals stall discovery velocity.

Practical Example: Discovery Velocity at Spotify

Spotify's "squad" model (2012 to 2023) was an organizational experiment in maximizing discovery velocity. Each squad owned a hypothesis about user behavior (for example, "users want algorithmically generated playlists based on listening history"). Squads ran independent experiments, measured outcomes, and shared learnings. The organizational structure was itself a parallel search strategy: multiple squads exploring different regions of the product space simultaneously, reducing total entropy faster than a single team working sequentially. When Spotify introduced Discover Weekly in 2015, it was the result of a squad testing the hypothesis that users wanted a weekly personalized playlist. The feature was confirmed through staged rollouts with engagement metrics as the success criterion.

5. The Expected Regret of Greedy Development

Discovery velocity tells us how fast a team is learning, but it does not yet explain what happens when a team skips exploration altogether and bets everything on the first plausible idea.

Why not just build the first idea that seems good? The answer lies in the regret analysis from Section 1.2. Greedy development, where the team commits fully to the highest-expected-value feature without exploring alternatives, incurs expected regret that grows with the uncertainty in the environment.

Define the expected regret of greedy development over \(T\) sprints as:

$$R_{\text{greedy}}(T) = \sum_{t=1}^{T} \left[ f(a^*_t) - f(a_t^{\text{greedy}}) \right]$$

where \(a^*_t\) is the optimal action at time \(t\) (knowable only in hindsight) and \(a_t^{\text{greedy}}\) is the action chosen by the greedy strategy. The key result from bandit theory, a branch of decision theory that studies how to allocate a fixed budget of attempts among competing options with unknown payoffs, is that greedy regret grows linearly with \(T\) when the environment is uncertain, while exploratory strategies (UCB, Thompson Sampling) achieve regret that grows only as \(O(\log T)\), scaling logarithmically rather than linearly with the number of rounds (Auer et al., 2002).

Real-World Application: Amazon's Single-Threaded Leadership
Real-World Application: Amazon's Single-Threaded Leadership

Checkpoint

So far: greedy development locks onto the first plausible idea, incurring regret that grows linearly with time, while exploratory strategies (UCB, Thompson Sampling) pay a small upfront cost to survey alternatives and achieve only logarithmic regret, wasting far less effort over a product's lifetime.

In software terms: a team that always builds the "obvious" next feature without testing alternatives accumulates more wasted effort over a product's lifetime. A team that invests some fraction of each sprint in exploratory prototypes discovers the best options sooner and wastes less overall. The following simulation illustrates this.

import numpy as np
import matplotlib.pyplot as plt


def simulate_dev_strategies(n_sprints: int = 50,
                            n_features: int = 10,
                            seed: int = 42) -> dict:
    """Simulate greedy vs. exploratory development over multiple sprints.

    Each 'feature' has a true value drawn from a distribution unknown to
    the team. The greedy strategy always picks the feature with the highest
    estimated value. The exploratory (UCB-style) strategy balances estimated
    value with uncertainty, exploring less-known features.

    Returns cumulative regret for each strategy.
    """
    rng = np.random.default_rng(seed)

    # True feature values (unknown to the team)
    true_values = rng.beta(2, 5, size=n_features)  # Skewed: most features mediocre
    true_values[rng.choice(n_features)] = 0.9       # One great feature, hidden

    # Team's estimates and observation counts
    estimates = np.full(n_features, 0.5)  # Uniform prior
    counts = np.zeros(n_features)

    greedy_regret = []
    ucb_regret = []
    optimal_value = true_values.max()

    greedy_total = 0.0
    ucb_total = 0.0

    ucb_estimates = estimates.copy()
    ucb_counts = np.zeros(n_features)

    for t in range(1, n_sprints + 1):
        # Greedy: pick the feature with highest current estimate
        greedy_choice = np.argmax(estimates)
        greedy_reward = true_values[greedy_choice] + rng.normal(0, 0.1)
        counts[greedy_choice] += 1
        # Update estimate (running average)
        estimates[greedy_choice] += (greedy_reward - estimates[greedy_choice]) / counts[greedy_choice]
        greedy_total += optimal_value - true_values[greedy_choice]
        greedy_regret.append(greedy_total)

        # UCB: balance exploitation and exploration
        ucb_scores = np.where(
            ucb_counts > 0,
            ucb_estimates + np.sqrt(2 * np.log(t) / ucb_counts),
            float('inf')  # Explore untried features first
        )
        ucb_choice = np.argmax(ucb_scores)
        ucb_reward = true_values[ucb_choice] + rng.normal(0, 0.1)
        ucb_counts[ucb_choice] += 1
        ucb_estimates[ucb_choice] += (ucb_reward - ucb_estimates[ucb_choice]) / ucb_counts[ucb_choice]
        ucb_total += optimal_value - true_values[ucb_choice]
        ucb_regret.append(ucb_total)

    return {
        "greedy_regret": greedy_regret,
        "ucb_regret": ucb_regret,
        "true_values": true_values,
        "sprints": list(range(1, n_sprints + 1))
    }


results = simulate_dev_strategies()

# The greedy strategy accumulates roughly linear regret,
# while UCB's regret flattens as it discovers the best feature
print(f"Greedy total regret after 50 sprints: {results['greedy_regret'][-1]:.2f}")
print(f"UCB total regret after 50 sprints:    {results['ucb_regret'][-1]:.2f}")
print(f"Regret ratio (greedy/UCB):            {results['greedy_regret'][-1] / max(results['ucb_regret'][-1], 0.01):.1f}x")
Listing 7.3: Greedy vs. UCB feature-selection simulation over 50 sprints, demonstrating linear regret growth for greedy and sublinear regret for UCB exploration.
Fun Note: The Sunk Cost Sprint

In development teams, greedy regret often compounds through sunk cost reasoning. After investing three sprints in the wrong feature, the team feels committed: "We've already built half of it, we should finish." This is the software equivalent of continuing to dig in the wrong location because you already have a deep hole. The hypothesis-driven approach provides a principled exit criterion: if the hypothesis is refuted, stop building, regardless of how much code you have written. Code is cheap; shipping the wrong product is expensive.

6. SDLC Models as Exploration Strategies

The regret analysis above treats exploration as a single knob to turn up or down, but real development teams encode their exploration choices into a methodology, each with its own feedback rhythm and risk profile.

Different SDLC methodologies correspond to different strategies for navigating the search space. We can characterize each by its exploration/exploitation ratio and the feedback loop length (how quickly hypotheses are tested).

Methodology Profiles

Waterfall is a pure exploitation strategy: commit to a single path through the search space (the specification) and follow it to completion. Feedback arrives only at deployment. In bandit terms, waterfall is equivalent to pulling the same arm for all \(T\) rounds without observing rewards. This works well when uncertainty is very low (the team has built similar systems before) but incurs catastrophic regret when assumptions are wrong.

Agile/Scrum introduces periodic exploration through sprint cycles. Each sprint is a mini-experiment: the team builds a small increment, gathers feedback, and adjusts. The sprint length (typically 1 to 4 weeks) determines the feedback loop latency. Shorter sprints mean faster hypothesis testing, but each individual experiment has less power (less code built, fewer features to evaluate).

Lean Startup pushes exploration further by minimizing the cost of each experiment. The minimum viable product (MVP) is the smallest possible test of the most critical hypothesis. Build-measure-learn cycles can be as short as hours or days. In bandit terms, lean development reduces the cost per arm pull, enabling more exploration within the same budget. Under the parameters in Listing 7.4, a Lean/MVP team can test roughly 60 hypotheses per quarter; a Waterfall team, following the same model, tests fewer than one.

Design Thinking adds a distinct generation phase (ideation) before testing. While agile and lean tend to test hypotheses sequentially, design thinking encourages generating a broad set of candidates before selecting any for implementation. This corresponds to a higher exploration budget in the early phases, trading short-term progress for a better initial survey of the search space.

from dataclasses import dataclass


@dataclass
class SDLCStrategy:
    """Characterize an SDLC methodology as a search strategy."""
    name: str
    exploration_ratio: float      # Fraction of effort spent exploring alternatives
    feedback_loop_days: float     # Average time to test one hypothesis
    cost_per_experiment: float    # Relative cost of each experiment (1.0 = full feature)
    parallelism: int              # Number of hypotheses tested simultaneously

    @property
    def hypotheses_per_quarter(self) -> float:
        """Estimate how many hypotheses a team can test in 90 days."""
        sequential = 90 / self.feedback_loop_days
        return sequential * self.parallelism

    @property
    def regret_profile(self) -> str:
        if self.exploration_ratio < 0.1:
            return "Linear (high risk of locking onto suboptimal path)"
        elif self.exploration_ratio < 0.3:
            return "Sublinear (moderate exploration mitigates lock-in)"
        else:
            return "Logarithmic (broad exploration, slower initial delivery)"


strategies = [
    SDLCStrategy("Waterfall",      0.05,  180,  1.0,   1),
    SDLCStrategy("Scrum (2-week)", 0.20,   14,  0.3,   1),
    SDLCStrategy("Lean/MVP",       0.35,    3,  0.05,  2),
    SDLCStrategy("Design Thinking",0.50,    7,  0.1,   3),
]

print(f"{'Strategy':<20} {'Explore%':>8} {'Loop(d)':>8} "
      f"{'Hyp/Qtr':>8}  {'Regret Profile'}")
print("-" * 85)
for s in strategies:
    print(f"{s.name:<20} {s.exploration_ratio:>7.0%} {s.feedback_loop_days:>8.0f} "
          f"{s.hypotheses_per_quarter:>8.0f}  {s.regret_profile}")
Listing 7.4: SDLCStrategy dataclass comparing Waterfall, Scrum, Lean/MVP, and Design Thinking by exploration ratio, feedback loop, and hypothesis throughput per quarter.

Real-World Application: Amazon's Single-Threaded Leadership

Amazon structures product teams around "single-threaded leaders," each owning one hypothesis about customer needs. When the Kindle team launched in 2007, the core hypothesis was that readers would pay for a dedicated e-ink device if it could deliver any book in under 60 seconds. Rather than building a full tablet, the team tested this narrow claim with a single-purpose device, treating the hardware itself as an MVP. The success criterion (sustained book purchases exceeding physical sales within two years) drove every design trade-off, from removing a color screen to prioritizing cellular connectivity over WiFi.

Research Frontier: Hypothesis-Driven Development in Industry (2024 to 2026)

The hypothesis-driven development (HDD) movement has gained significant traction in industry since 2023. Microsoft's experimentation platform (ExP) runs over 20,000 concurrent A/B tests, treating every feature change as a hypothesis with an explicit success metric (Gupta et al., 2019). Netflix's "scientific approach to content decisions" applies Bayesian updating to programming choices, modeling each show as a hypothesis about audience preferences. Most recently, the integration of LLM-based code generation (Copilot, Cursor, Claude Code) has further reduced the cost of building experimental prototypes, making it feasible to test hypotheses that would previously have been too expensive to implement. A notable 2024 advance is the EDELTA framework (Ye et al., "Automated Hypothesis Testing with Large Language Models," ICSE 2024), which uses LLMs to automatically generate, prioritize, and evaluate development hypotheses from issue trackers and code repositories, closing the loop between hypothesis formulation and empirical validation without manual experiment design. We explore the broader AI acceleration effect in detail in Section 7.2 and throughout Chapter 9: Vibe Coding.

Library Shortcut: Hypothesis Tracking with GitHub Issues

The DevHypothesis class above demonstrates the concept from scratch, but in practice you do not need to build your own tracking system. GitHub Issues with a custom "hypothesis" label, structured templates, and project boards provide the same workflow in roughly 10 lines of configuration versus the 80 lines of Python above. Create an issue template with fields for Claim, Test Procedure, Success Criterion, and Confidence; use labels for status (untested, testing, confirmed, refuted); and use GitHub Projects to visualize the hypothesis portfolio as a Kanban board. The gh CLI can automate creation and updates. For large-scale experimentation platforms, tools like LaunchDarkly, Split.io, or Statsig provide built-in hypothesis management with statistical rigor.

Try It: Track and Visualize Discovery Velocity for a To-Do App

Build a small hypothesis tracker and measure discovery velocity across simulated sprints. You need only Python and matplotlib (or any plotting library).

  1. Create a JSON file listing five hypotheses for a to-do app (for example: "users want recurring tasks," "drag-and-drop reordering increases engagement," "dark mode reduces evening churn"). Give each hypothesis a claim, test procedure, success criterion, and initial confidence of 0.5.
  2. Write a Python script that loads the hypotheses, then simulates three two-week sprints. In each sprint, pick one hypothesis at random, generate a synthetic observation (confirm with probability 0.6, refute with probability 0.4), and apply the Bayesian confidence update from Listing 7.1.
  3. After each sprint, compute the total entropy across all hypotheses using the formula from Listing 7.2. Store the entropy value and the sprint number.
  4. Plot entropy versus sprint number using matplotlib. Add a horizontal line at the initial entropy so you can see the rate of decrease. Label the plot "Discovery Velocity" and annotate the slope of the curve.
  5. Experiment with two variants: (a) always test the hypothesis with the highest uncertainty first (max-entropy selection) and (b) always test the same hypothesis repeatedly (greedy selection). Plot all three strategies on one chart and compare their convergence rates.

Lab: Greedy vs. Exploratory Feature Selection

Goal: Observe how different exploration strategies affect cumulative regret when selecting features to build, replicating the simulation from Listing 7.3 and extending it with your own twist.
Tools needed: Python 3, NumPy, matplotlib (all in the standard scientific stack).
Procedure (15 to 30 minutes):

  1. Copy the simulate_dev_strategies function from Listing 7.3 into a script. Run it and plot the two regret curves (greedy vs. UCB) to confirm the baseline.
  2. Add a Thompson Sampling strategy: for each feature, sample from a Beta distribution parameterized by observed successes and failures, then pick the feature with the highest sample. Track its cumulative regret alongside the other two.
  3. What to vary: Change n_features from 5 to 50, change the true-value distribution from Beta(2, 5) to Beta(1, 1) (uniform), and try seeds 0 through 9. Record how each change affects the sprint at which UCB and Thompson Sampling first beat greedy.
  4. What to observe: At what sprint count does each strategy's regret curve flatten? How does the number of features affect the exploration cost (early regret) of UCB and Thompson Sampling? Is there a setting where greedy actually wins?

Write a one-paragraph summary of which strategy you would recommend for a team with a 20-sprint budget and 15 candidate features, citing your experimental evidence.

Exercises

  1. (Conceptual) Take a software project you have worked on (or are currently working on). Identify three hypotheses that the team held implicitly. For each, write the claim, test procedure, and success criterion in the DevHypothesis format. Were any of these hypotheses tested before full implementation? What would have changed if they had been?
  2. (Coding) Extend the simulate_dev_strategies function to include a Thompson Sampling strategy alongside greedy and UCB. Compare the regret curves of all three over 100 sprints. Which strategy discovers the best feature fastest? At what sprint count does each strategy's regret curve begin to flatten?
  3. (Analysis) Compute the discovery velocity for two hypothetical teams: Team A runs 2-week sprints and resolves one hypothesis per sprint; Team B runs 1-week sprints but resolves only 0.4 hypotheses per sprint (due to lower experiment power). Which team has higher discovery velocity? Under what conditions does Team B's shorter cycle outweigh its lower per-cycle resolution rate?

What's Next

With the SDLC reframed as hypothesis-driven search, Section 7.2: Engineering Discovery and AI examines how AI tools change the economics of this discovery process. When prototype costs drop from days to minutes, the optimal exploration strategy shifts dramatically.

Bibliography

Ries, E. (2011). The Lean Startup. Crown Business.

The build-measure-learn loop as a systematic approach to validated learning.

Gupta, S., et al. (2019). Top challenges from the first practical online controlled experiments summit. KDD Explorations, 21(1), 20-35.

Lessons from Microsoft, Google, LinkedIn, and Netflix on running thousands of concurrent A/B experiments.

Auer, P., Cesa-Bianchi, N., & Fischer, P. (2002). Finite-time analysis of the multiarmed bandit problem. Machine Learning, 47(2), 235-256.

The UCB1 algorithm and its logarithmic regret bound, foundational to the regret analysis in this section.