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

7.3 Building a Hypothesis-Driven Dev Workflow

"My test suite has a 100% pass rate. Unfortunately, it tests none of the hypotheses that actually matter."

A Test Suite With Existential Doubts

Prerequisites

This section is the hands-on recipe that ties together the theory from Section 7.1 (software development lifecycle, or SDLC, as hypothesis testing) and Section 7.2 (AI-accelerated discovery). You will need Python 3.10+, Git, and pytest installed. We also build on the Discovery Workbench scaffold from Chapter 6, extending it with a development hypothesis tracker. Familiarity with Git branching (creating branches, making commits, merging) is assumed at a basic level.

The Big Picture

Theory without practice is philosophy. This section is the laboratory session where you build a complete hypothesis-driven development workflow from scratch. Starting with nothing more than a vague product idea ("build a tool that helps researchers discover relevant papers"), you will decompose the idea into falsifiable hypotheses, convert confirmed hypotheses into user stories, write skeleton tests that encode success criteria, and organize everything in a Git repository that functions as both a scientific lab notebook and a production codebase. Every tool we use (Git, pytest, GitHub Issues) maps directly onto a concept from the previous two sections. By the end, you will have a reusable template for any software project that takes discovery seriously.

1. The Recipe: From Vague Idea to Executable Hypotheses

Teams that skip structured decomposition often discover, months into a build, that they solved the wrong problem or optimized the wrong metric. A single afternoon of hypothesis formulation can, in many cases, prevent weeks or months of misdirected engineering. Imagine it is Monday morning and your team has exactly one sentence on the whiteboard: "Build a tool that helps researchers discover relevant papers." By Friday, that sentence will either still be a sentence, or it will be a Git repository with falsifiable claims (where "falsifiable" means each claim is stated precisely enough that a concrete experiment could prove it wrong), prioritized experiments, and skeleton tests that know precisely what "success" looks like. The difference is a five-stage pipeline, illustrated in Figure 7.3 below. Each stage produces an artifact that the next stage consumes, and a team can execute the entire sequence in a single afternoon for a new project.

Five-Stage Hypothesis-Driven Development Pipeline 1. Decompose Break idea into uncertainty categories (Listing 7.11) uncertainties 2. Formulate Write falsifiable hypotheses (Listing 7.12) hypotheses 3. Prioritize Rank by info value (uncertainty x fan-out) (Listing 7.13) ranked queue 4. Scaffold Write skeleton pytest tests (Listing 7.14) test suite 5. Organize Git repo + CI as lab notebook (Listings 7.15-16) Evidence feeds back: confirmed hypotheses merge to main; refuted hypotheses spawn new decomposition Outputs per stage Uncertainty list DevHypothesis objects Priority queue Skipped tests Branches + CI
Figure 7.3: The five-stage hypothesis-driven development pipeline. Each stage produces a concrete artifact (uncertainties, hypotheses, a ranked priority queue, skeleton tests, and a Git repository with CI) that the next stage consumes. The dashed feedback loop shows how experiment outcomes trigger new decomposition rounds.
  1. Idea decomposition: break the vague idea into distinct uncertainty categories (user needs, technical feasibility, business value, integration constraints).
  2. Hypothesis formulation: for each uncertainty, write a falsifiable claim with a test procedure and success criterion.
  3. Prioritization: rank hypotheses by information value (a composite score combining how uncertain a hypothesis is with how many downstream decisions depend on its answer).
  4. Test scaffolding: write skeleton pytest tests that encode the success criteria. These tests fail initially (red) and pass only when the hypothesis is confirmed through implementation (green).
  5. Repository structure: organize everything in a Git repo with branches for each hypothesis experiment, a hypothesis registry file, and continuous integration (CI) that runs the test suite on every push.

The following stages use a running example: a scientific paper recommendation engine for the Discovery Workbench.

2. Stage 1: Idea Decomposition

The vague idea is: "Build a tool that helps researchers discover relevant papers they would not find through keyword search alone." This single sentence conceals at least a dozen uncertainties. We systematically decompose it using the four categories from Section 7.1.

"""Stage 1: Decompose a vague product idea into structured uncertainties.

This module provides a systematic approach to breaking down product ideas
into the four uncertainty categories that drive hypothesis generation.
"""

from dataclasses import dataclass, field
from typing import List
from enum import Enum


class UncertaintyCategory(Enum):
    USER = "user_needs"           # What do users actually want?
    TECHNICAL = "technical"       # What technical approach will work?
    VALUE = "value"               # Will this deliver measurable value?
    INTEGRATION = "integration"   # How does this fit existing workflows?


@dataclass
class Uncertainty:
    """A single uncertainty extracted from a product idea."""
    question: str
    category: UncertaintyCategory
    assumptions: List[str] = field(default_factory=list)
    impact_if_wrong: str = ""     # What happens if our assumption is incorrect?


@dataclass
class IdeaDecomposition:
    """Structured decomposition of a vague product idea."""
    original_idea: str
    uncertainties: List[Uncertainty] = field(default_factory=list)

    def add(self, question: str, category: UncertaintyCategory,
            assumptions: List[str], impact: str) -> None:
        self.uncertainties.append(Uncertainty(
            question=question,
            category=category,
            assumptions=assumptions,
            impact_if_wrong=impact
        ))

    def summary(self) -> str:
        lines = [f"Idea: {self.original_idea}", ""]
        for cat in UncertaintyCategory:
            items = [u for u in self.uncertainties if u.category == cat]
            if items:
                lines.append(f"  {cat.value} ({len(items)} uncertainties):")
                for u in items:
                    lines.append(f"    - {u.question}")
        lines.append(f"\n  Total uncertainties: {len(self.uncertainties)}")
        return "\n".join(lines)


# Decompose the paper recommendation idea
decomp = IdeaDecomposition(
    "Build a tool that helps researchers discover relevant papers "
    "they would not find through keyword search alone"
)

decomp.add(
    "Do researchers want serendipitous discovery or targeted recommendations?",
    UncertaintyCategory.USER,
    ["Researchers value novelty", "Current search tools are insufficient"],
    "Building for serendipity when users want precision wastes the entire product"
)

decomp.add(
    "Do researchers trust algorithmic recommendations for scientific reading?",
    UncertaintyCategory.USER,
    ["Trust in AI recommendations transfers from consumer to scientific domains"],
    "Low trust means low adoption regardless of recommendation quality"
)

decomp.add(
    "Which similarity metric best captures 'relevance' for scientific papers?",
    UncertaintyCategory.TECHNICAL,
    ["Embedding similarity correlates with human relevance judgments"],
    "Wrong similarity metric produces irrelevant recommendations"
)

decomp.add(
    "Can we achieve sub-200ms latency with dense retrieval over 200M papers?",
    UncertaintyCategory.TECHNICAL,
    ["Approximate nearest neighbor search scales to this corpus size"],
    "High latency kills interactive use; must fall back to batch recommendations"
)

decomp.add(
    "Will recommendations lead to papers being read, or just bookmarked?",
    UncertaintyCategory.VALUE,
    ["Click-through rate is a valid proxy for reading"],
    "Optimizing for clicks produces clickbait, not useful recommendations"
)

decomp.add(
    "Can we integrate with Zotero and Mendeley without breaking their APIs?",
    UncertaintyCategory.INTEGRATION,
    ["Reference manager APIs are stable and well-documented"],
    "No integration means users must manually track recommendations"
)

print(decomp.summary())
Listing 7.11: Decomposing the paper recommendation idea into six structured uncertainties across four categories (user needs, technical feasibility, business value, integration), each with explicit assumptions and impact statements.
Key Insight: Assumptions Are the Hidden Hypotheses

Every uncertainty contains hidden assumptions. The question "Which similarity metric works best?" assumes that similarity-based retrieval is the right approach at all. The question "Can we integrate with Zotero?" assumes that reference manager integration is necessary for adoption. Surfacing these assumptions is critical because the highest-impact hypotheses are often about the assumptions, not the surface-level questions. A hypothesis that challenges a foundational assumption ("Do researchers even want recommendations, or do they want better search?") can redirect the entire project. This is why design thinking starts with empathy research before ideation, and why the decomposition stage should always include a step where the team explicitly lists and questions its own assumptions.

3. Stage 2: Hypothesis Formulation

Each uncertainty becomes one or more hypotheses using the DevHypothesis class from Section 7.1. The critical discipline is making each hypothesis falsifiable: it must be possible to run an experiment whose outcome could prove the hypothesis wrong. "Users will like our recommendations" is not falsifiable. "At least 20% of users will click on at least one recommendation per session" is falsifiable.

"""Stage 2: Convert uncertainties into falsifiable DevHypotheses.

Each hypothesis must have:
- A specific, falsifiable claim
- A concrete test procedure achievable within one sprint
- A measurable success criterion with a numerical threshold
"""

import json
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass, field, asdict
from typing import List


@dataclass
class DevHypothesis:
    """A falsifiable development hypothesis (reproduced from Section 7.1)."""
    id: str
    claim: str
    category: str
    test_procedure: str
    success_criterion: str
    confidence: float = 0.5
    evidence: list = field(default_factory=list)
    status: str = "untested"
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())
    branch: str = ""              # Git branch for this experiment
    depends_on: List[str] = field(default_factory=list)

    def update_confidence(self, observation: str, confirms: bool,
                          strength: float = 0.1) -> float:
        if confirms:
            self.confidence += strength * (1.0 - self.confidence)
        else:
            self.confidence -= strength * self.confidence

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

        if self.confidence > 0.9:
            self.status = "confirmed"
        elif self.confidence < 0.1:
            self.status = "refuted"
        else:
            self.status = "testing"
        return self.confidence


@dataclass
class HypothesisRegistry:
    """A persistent registry of all hypotheses for a project.

    Stored as a JSON file in the repository root, this serves as the
    project's 'lab notebook' tracking what the team believes and why.
    """
    project_name: str
    hypotheses: List[DevHypothesis] = field(default_factory=list)

    def add(self, hypothesis: DevHypothesis) -> None:
        self.hypotheses.append(hypothesis)

    def save(self, path: Path) -> None:
        """Persist the registry to a JSON file (committed to Git)."""
        data = {
            "project": self.project_name,
            "updated_at": datetime.now().isoformat(),
            "hypotheses": [asdict(h) for h in self.hypotheses]
        }
        path.write_text(json.dumps(data, indent=2))

    @classmethod
    def load(cls, path: Path) -> "HypothesisRegistry":
        """Load registry from a JSON file."""
        data = json.loads(path.read_text())
        registry = cls(project_name=data["project"])
        for h_data in data["hypotheses"]:
            registry.add(DevHypothesis(**h_data))
        return registry

    def active(self) -> List[DevHypothesis]:
        return [h for h in self.hypotheses if h.status in ("untested", "testing")]

    def dashboard(self) -> str:
        """Print a summary dashboard of hypothesis status."""
        lines = [f"Hypothesis Registry: {self.project_name}", "=" * 60]
        status_counts = {}
        for h in self.hypotheses:
            status_counts[h.status] = status_counts.get(h.status, 0) + 1

        for status, count in sorted(status_counts.items()):
            lines.append(f"  {status:<12}: {count}")

        lines.append(f"  {'total':<12}: {len(self.hypotheses)}")
        lines.append("")

        for h in self.hypotheses:
            marker = {"untested": "[ ]", "testing": "[~]",
                      "confirmed": "[+]", "refuted": "[-]"}.get(h.status, "[?]")
            lines.append(f"  {marker} {h.id}: {h.claim[:55]}...")
            lines.append(f"      confidence: {h.confidence:.2f}  |  "
                         f"evidence: {len(h.evidence)} observations")
        return "\n".join(lines)


# Build the registry for our paper recommendation project
registry = HypothesisRegistry("paper-recommender")

registry.add(DevHypothesis(
    id="H-001",
    claim="At least 20% of researchers click on a recommendation per session",
    category="value",
    test_procedure="Deploy MVP to 100 beta users for 2 weeks; track CTR",
    success_criterion="Session CTR >= 0.20",
    confidence=0.5,
    branch="experiment/h001-ctr-baseline"
))

registry.add(DevHypothesis(
    id="H-002",
    claim="SPECTER2 embeddings produce more relevant results than BM25",
    category="implementation",
    test_procedure="Offline evaluation on TREC-COVID benchmark",
    success_criterion="SPECTER2 nDCG@10 exceeds BM25 nDCG@10 by >0.05",
    confidence=0.65,
    branch="experiment/h002-specter-vs-bm25",
    depends_on=[]
))

registry.add(DevHypothesis(
    id="H-003",
    claim="Approximate nearest neighbor search achieves p95 < 200ms on 50M papers",
    category="implementation",
    test_procedure="Benchmark FAISS HNSW index with 50M SPECTER2 vectors",
    success_criterion="p95 latency < 200ms with recall@10 > 0.95",
    confidence=0.7,
    branch="experiment/h003-faiss-latency",
    depends_on=["H-002"]  # Only relevant if we use embeddings
))

registry.add(DevHypothesis(
    id="H-004",
    claim="Researchers prefer citation-augmented recs over pure content similarity",
    category="requirement",
    test_procedure="Within-subjects A/B test: 50 users rate both approaches",
    success_criterion="Mean preference score for citation-augmented > 3.5/5.0",
    confidence=0.55,
    branch="experiment/h004-citation-augmented",
    depends_on=["H-002"]
))

registry.add(DevHypothesis(
    id="H-005",
    claim="Zotero plugin integration increases weekly active usage by >30%",
    category="integration",
    test_procedure="Release Zotero plugin to 50% of beta users; compare WAU",
    success_criterion="WAU ratio (with plugin / without) > 1.30",
    confidence=0.45,
    branch="experiment/h005-zotero-integration",
    depends_on=["H-001"]  # Only matters if baseline engagement exists
))

print(registry.dashboard())
Listing 7.12: Building a HypothesisRegistry with five falsifiable hypotheses for the paper recommender, each specifying a Git experiment branch, dependency links, and a Bayesian confidence update method.

Several hypotheses above reference domain-specific tools: SPECTER2 is a transformer-based embedding model trained specifically on scientific documents; BM25 is a classic term-frequency retrieval baseline widely used in search engines; and nDCG@10 (normalized discounted cumulative gain at rank 10) is a standard metric that measures how well a ranked list places relevant results near the top. You do not need deep familiarity with these tools to follow the workflow; what matters is that each hypothesis names a concrete method and a measurable threshold.

Checkpoint

So far: you have decomposed a vague idea into structured uncertainties (Stage 1) and converted those uncertainties into falsifiable hypotheses stored in a persistent registry with dependency links and confidence tracking (Stage 2). Next, you will learn how to decide which hypothesis to test first.

4. Stage 3: Prioritization by Information Value

With five hypotheses in the registry, the team must decide which to test first. The prioritization logic from Section 7.2 combines uncertainty with dependency fan-out. Uncertainty is quantified here using binary entropy, an information-theory measure that peaks at 1.0 when confidence is 50/50 and drops toward 0 as confidence approaches certainty in either direction. The formula then factors in test cost: a cheap test with moderate information value often beats an expensive test with marginally higher information value.

Mental Model

Think of hypothesis prioritization like triage in an emergency room. A doctor does not treat patients in the order they arrive; instead, each patient is scored by severity (how sick they are) multiplied by urgency (how many other treatments depend on stabilizing this patient first). A patient with moderate symptoms but whose condition blocks three pending surgeries gets seen before a sicker patient whose case is self-contained. In the same way, a hypothesis with moderate uncertainty but high dependency fan-out (many other hypotheses depend on its answer) rises above a more uncertain hypothesis that stands alone. The "category weight" acts like a flag for conditions that could be life-threatening: value and requirement hypotheses can kill the entire project, so they receive the equivalent of an elevated triage code.

import numpy as np
from typing import List, Tuple


def information_value(hypothesis: DevHypothesis,
                      all_hypotheses: List[DevHypothesis]) -> float:
    """Compute the information value of testing a hypothesis.

    Combines three factors:
    1. Uncertainty: binary entropy, maximized when confidence = 0.5
    2. Fan-out: number of other hypotheses that depend on this one
    3. Category weight: requirement and value hypotheses get a bonus
       because they can redirect the entire project
    """
    # Factor 1: Uncertainty (0 to 1)
    c = np.clip(hypothesis.confidence, 1e-10, 1 - 1e-10)
    uncertainty = -(c * np.log2(c) + (1 - c) * np.log2(1 - c))

    # Factor 2: Dependency fan-out
    dependents = sum(
        1 for h in all_hypotheses
        if hypothesis.id in h.depends_on
    )
    fan_out = 1.0 + 0.4 * dependents

    # Factor 3: Category weight
    category_weights = {
        "requirement": 1.5,  # Can redirect the whole project
        "value": 1.3,        # Determines if the project is worth doing
        "implementation": 1.0,
        "integration": 0.8,
    }
    cat_weight = category_weights.get(hypothesis.category, 1.0)

    return uncertainty * fan_out * cat_weight


def prioritize(registry: HypothesisRegistry) -> List[Tuple[float, DevHypothesis]]:
    """Rank active hypotheses by information value, descending."""
    active = registry.active()
    scored = [
        (information_value(h, registry.hypotheses), h)
        for h in active
    ]
    scored.sort(key=lambda x: x[0], reverse=True)
    return scored


ranked = prioritize(registry)

print("Hypothesis Priority Queue")
print("=" * 70)
print(f"{'Rank':<6} {'ID':<8} {'InfoVal':>8} {'Conf':>6} {'Category':<16} {'Claim'}")
print("-" * 70)
for i, (score, h) in enumerate(ranked, 1):
    print(f"{i:<6} {h.id:<8} {score:>8.3f} {h.confidence:>5.2f} "
          f"{h.category:<16} {h.claim[:35]}...")
Listing 7.13: Scoring each hypothesis by binary entropy, dependency fan-out, and category weight, then sorting the active queue to determine the optimal testing order.

Step-Through: Information Value Calculation

Trace through information_value for hypothesis H-001 (confidence = 0.50, category = "value", zero dependents) and H-002 (confidence = 0.65, category = "implementation", two dependents: H-003 and H-004).

H-001: Uncertainty = −(0.50 log2 0.50 + 0.50 log2 0.50) = 1.000. Fan-out = 1.0 + 0.4 × 0 = 1.0 (H-005 depends on it, so actually fan-out = 1.0 + 0.4 × 1 = 1.4). Category weight for "value" = 1.3. Score = 1.000 × 1.4 × 1.3 = 1.820.

H-002: Uncertainty = −(0.65 log2 0.65 + 0.35 log2 0.35) = 0.934. Fan-out = 1.0 + 0.4 × 2 = 1.8 (H-003 and H-004 both depend on it). Category weight for "implementation" = 1.0. Score = 0.934 × 1.8 × 1.0 = 1.681.

Result: H-001 ranks first despite lower fan-out because its maximum uncertainty (0.50) combined with the "value" category bonus outweighs H-002's higher fan-out. If H-002 had three dependents instead of two, its score would be 0.934 × 2.2 × 1.0 = 2.055, and it would jump to the top.

Practical Example: The Hypothesis That Saved Six Months

A data science team at a pharmaceutical company was building an automated clinical trial matching system. Their initial plan allocated three months to building a sophisticated natural language processing (NLP) pipeline for parsing trial eligibility criteria. One team member proposed testing a hypothesis first: "Clinicians will trust automated matches enough to act on them without manual verification." They built a simple mockup in two days, showed it to 15 clinicians, and discovered that the answer was no: clinicians wanted the system to surface candidates, not to make matches. This refuted the value hypothesis and redirected the project from "automated matching" to "assisted search," saving an estimated six months of building the wrong thing. The total cost of the experiment was two days; the cost of not running it would have been six months of wasted engineering.

5. Stage 4: Skeleton Tests as Falsification Instruments

A central practice in hypothesis-driven development is writing tests before implementation, not as a coding discipline (TDD) but as a scientific discipline. Each test encodes the success criterion of a hypothesis. When the test passes, the hypothesis has survived falsification. When it fails, the hypothesis is either refuted or the implementation is incomplete.

Common Misconception

Many developers assume that hypothesis-driven testing is just Test-Driven Development (TDD) with different vocabulary. It is not. In TDD, a failing test always means "the code is wrong" and the goal is to make every test pass; in hypothesis-driven development, a failing test can mean "the hypothesis is wrong," and the correct response may be to delete the feature branch, record the refutation as evidence, and pivot to a different approach. Treating every red test as a bug to fix, rather than a possible signal to change direction, defeats the entire purpose of the framework.

Because a failing test in this framework carries a fundamentally different meaning than in conventional TDD, the tests themselves need a different structure, one designed to encode falsifiable criteria rather than implementation contracts.

Skeleton Tests: Shape Before Substance

We call these skeleton tests because they define the shape of the expected behavior without implementing the internals. They are written at the same time as the hypothesis, before any production code exists. This practice forces the team to make success criteria concrete and measurable before investing in implementation.

A skeleton test is a fully structured test function with imports, assertions, and threshold constants. Its body references modules and classes that do not yet exist. The developer marks it pytest.mark.skip or xfail so the suite stays green while production code is absent. Skeleton tests translate a vague success criterion ("embedding quality should be good enough") into an unambiguous, machine-checkable contract before writing a single line of production code. This eliminates the common failure mode where the team builds first and retrofits acceptance criteria later. When a developer begins work on a hypothesis, they remove the skip marker, run the test, watch it fail (red), implement until it passes (green), and then record the result as evidence in the hypothesis registry. Use skeleton tests whenever the success criterion is quantitative and automatable; for qualitative criteria (such as user interview insights or design review feedback), record evidence manually in the registry instead.

"""
Stage 4: Skeleton tests for each hypothesis.

File: tests/test_hypotheses.py

These tests encode the success criteria from the hypothesis registry.
Each test is initially marked with pytest.mark.skip or xfail because
the implementation does not exist yet. As implementation proceeds,
tests are unskipped and expected to pass.
"""

import pytest
import numpy as np


# --------------- H-002: SPECTER2 vs BM25 ---------------

class TestH002_EmbeddingQuality:
    """Hypothesis: SPECTER2 produces more relevant results than BM25.
    Success criterion: nDCG@10 improvement > 0.05 on TREC-COVID."""

    NDCG_IMPROVEMENT_THRESHOLD = 0.05

    @pytest.fixture
    def trec_covid_queries(self):
        """Load TREC-COVID benchmark queries and relevance judgments."""
        # In production, this loads from the actual dataset
        # Skeleton returns a minimal test fixture
        return {
            "queries": [
                "coronavirus origin",
                "COVID-19 vaccine efficacy",
                "SARS-CoV-2 transmission dynamics",
            ],
            "qrels": {
                "coronavirus origin": {"doc_001": 2, "doc_015": 1, "doc_042": 0},
                "COVID-19 vaccine efficacy": {"doc_007": 2, "doc_023": 1},
                "SARS-CoV-2 transmission dynamics": {"doc_003": 2, "doc_011": 2},
            }
        }

    @pytest.mark.skip(reason="H-002: Awaiting SPECTER2 and BM25 retriever implementations")
    def test_specter2_beats_bm25_ndcg(self, trec_covid_queries):
        """SPECTER2 nDCG@10 must exceed BM25 nDCG@10 by threshold."""
        from paper_recommender.retrievers import SPECTER2Retriever, BM25Retriever
        from paper_recommender.evaluation import compute_ndcg

        specter = SPECTER2Retriever()
        bm25 = BM25Retriever()

        specter_scores = []
        bm25_scores = []

        for query in trec_covid_queries["queries"]:
            qrels = trec_covid_queries["qrels"][query]
            specter_results = specter.retrieve(query, k=10)
            bm25_results = bm25.retrieve(query, k=10)

            specter_scores.append(compute_ndcg(specter_results, qrels, k=10))
            bm25_scores.append(compute_ndcg(bm25_results, qrels, k=10))

        improvement = np.mean(specter_scores) - np.mean(bm25_scores)

        assert improvement > self.NDCG_IMPROVEMENT_THRESHOLD, (
            f"SPECTER2 improvement ({improvement:.4f}) did not exceed "
            f"threshold ({self.NDCG_IMPROVEMENT_THRESHOLD}). "
            f"Hypothesis H-002 may need to be refuted."
        )


# --------------- H-003: FAISS Latency ---------------

class TestH003_RetrievalLatency:
    """Hypothesis: ANN search achieves p95 < 200ms on 50M vectors.
    Success criterion: p95 latency < 200ms with recall@10 > 0.95."""

    LATENCY_P95_MS = 200
    RECALL_THRESHOLD = 0.95

    @pytest.mark.skip(reason="H-003: Awaiting FAISS index with 50M vectors")
    def test_faiss_latency_under_threshold(self):
        """p95 query latency must be under 200ms."""
        import time
        from paper_recommender.index import FAISSIndex

        index = FAISSIndex.load("indices/specter2_50m.faiss")
        query_vectors = np.random.randn(1000, 768).astype(np.float32)

        latencies = []
        for qv in query_vectors:
            start = time.perf_counter()
            index.search(qv.reshape(1, -1), k=10)
            elapsed_ms = (time.perf_counter() - start) * 1000
            latencies.append(elapsed_ms)

        p95 = np.percentile(latencies, 95)
        assert p95 < self.LATENCY_P95_MS, (
            f"p95 latency ({p95:.1f}ms) exceeds threshold ({self.LATENCY_P95_MS}ms). "
            f"H-003 refuted: need to optimize index or reduce corpus size."
        )

    @pytest.mark.skip(reason="H-003: Awaiting FAISS index with 50M vectors")
    def test_faiss_recall_above_threshold(self):
        """Approximate search recall@10 must exceed 0.95 vs exact search."""
        from paper_recommender.index import FAISSIndex

        index = FAISSIndex.load("indices/specter2_50m.faiss")
        # Compare approximate results against exact brute-force results
        query_vectors = np.random.randn(100, 768).astype(np.float32)

        recalls = []
        for qv in query_vectors:
            approx_ids = set(index.search(qv.reshape(1, -1), k=10)[1][0])
            exact_ids = set(index.search_exact(qv.reshape(1, -1), k=10)[1][0])
            recalls.append(len(approx_ids & exact_ids) / 10)

        mean_recall = np.mean(recalls)
        assert mean_recall > self.RECALL_THRESHOLD, (
            f"Mean recall ({mean_recall:.3f}) below threshold ({self.RECALL_THRESHOLD}). "
            f"H-003 partially refuted: latency may be fine but recall is insufficient."
        )


# --------------- H-001: User Engagement ---------------

class TestH001_UserEngagement:
    """Hypothesis: At least 20% of researchers click on a recommendation.
    Success criterion: session CTR >= 0.20."""

    CTR_THRESHOLD = 0.20

    @pytest.mark.skip(reason="H-001: Awaiting beta deployment and 2-week data collection")
    def test_session_ctr_above_threshold(self):
        """Session click-through rate must exceed 20%."""
        from paper_recommender.analytics import load_engagement_data

        data = load_engagement_data("beta_v1", min_days=14)
        sessions_with_clicks = sum(1 for s in data if s["clicked_any"])
        total_sessions = len(data)
        ctr = sessions_with_clicks / total_sessions

        assert ctr >= self.CTR_THRESHOLD, (
            f"Session CTR ({ctr:.2%}) below threshold ({self.CTR_THRESHOLD:.0%}). "
            f"H-001 refuted: users are not engaging with recommendations."
        )

        # Also check statistical significance
        from scipy import stats
        # One-sample proportion test against threshold
        z_stat = (ctr - self.CTR_THRESHOLD) / np.sqrt(
            self.CTR_THRESHOLD * (1 - self.CTR_THRESHOLD) / total_sessions
        )
        p_value = 1 - stats.norm.cdf(z_stat)
        assert p_value < 0.05, (
            f"CTR exceeds threshold but is not statistically significant "
            f"(p={p_value:.4f}). Collect more data before confirming H-001."
        )
Listing 7.14: Three skeleton test classes encoding the success criteria for H-001 (session CTR), H-002 (SPECTER2, a transformer-based embedding model for scientific documents, vs. BM25, a classic term-frequency retrieval baseline, nDCG@10, or normalized discounted cumulative gain at rank 10, a standard metric for ranking quality), and H-003 (FAISS, Facebook's library for fast approximate nearest neighbor (ANN) search on dense vectors, latency).
Fun Note: Red Means Learning

In traditional TDD, a red test means "something is broken." In hypothesis-driven development, a red test means "we learned something." When test test_specter2_beats_bm25_ndcg fails because the improvement is only 0.03 (below the 0.05 threshold), that is not a bug. It is evidence. The hypothesis is refuted, and the team now knows to explore a different retrieval approach. The test did its job: it falsified a hypothesis cheaply, before the team built an entire product around the wrong embedding model. The most valuable test is often the one that fails.

Real-World Application: Spotify's Controlled Experimentation Platform
Real-World Application: Spotify's Controlled Experimentation Platform

6. Stage 5: Git as a Lab Notebook

The final stage organizes everything into a Git repository that serves dual duty as a lab notebook (recording what was tried and what was learned) and a production codebase (containing the software that survived testing). The branching strategy directly maps to the hypothesis framework.

"""
Stage 5: Repository structure and Git workflow for hypothesis-driven development.

This module provides utilities for managing the Git-based hypothesis workflow.
It creates branches for experiments, tracks results in commits, and generates
a development log from the Git history.
"""

import subprocess
import json
from pathlib import Path
from typing import Optional


class HypothesisGitWorkflow:
    """Manages Git operations for hypothesis-driven development.

    Branch naming convention:
      - main:                       production-ready, confirmed code
      - experiment/: experimental branch for testing
      - feature/:      confirmed hypothesis, being productionized

    Commit message convention:
      - [H-XXX] hypothesis: 
      - [H-XXX] evidence: 
      - [H-XXX] confirmed: 
      - [H-XXX] refuted: 
    """

    def __init__(self, repo_path: Path):
        self.repo_path = repo_path

    def _git(self, *args: str) -> str:
        """Run a git command and return stdout."""
        result = subprocess.run(
            ["git", *args],
            cwd=self.repo_path,
            capture_output=True, text=True, check=True
        )
        return result.stdout.strip()

    def start_experiment(self, hypothesis_id: str, claim: str) -> str:
        """Create a new experiment branch and commit the hypothesis."""
        branch = f"experiment/{hypothesis_id.lower()}"
        self._git("checkout", "-b", branch)
        self._git("commit", "--allow-empty",
                  "-m", f"[{hypothesis_id}] hypothesis: {claim}")
        return branch

    def record_evidence(self, hypothesis_id: str,
                        observation: str, confirms: bool) -> None:
        """Commit an evidence observation to the experiment branch."""
        verdict = "supports" if confirms else "contradicts"
        self._git("add", "-A")
        self._git("commit", "-m",
                  f"[{hypothesis_id}] evidence ({verdict}): {observation}")

    def conclude_experiment(self, hypothesis_id: str,
                            confirmed: bool, summary: str) -> None:
        """Mark the experiment as concluded with a final commit."""
        status = "confirmed" if confirmed else "refuted"
        self._git("add", "-A")
        self._git("commit", "-m",
                  f"[{hypothesis_id}] {status}: {summary}")

        if confirmed:
            # Merge confirmed experiments into main
            self._git("checkout", "main")
            self._git("merge", f"experiment/{hypothesis_id.lower()}",
                      "--no-ff", "-m",
                      f"Merge confirmed experiment {hypothesis_id}: {summary}")

    def experiment_log(self, hypothesis_id: Optional[str] = None) -> str:
        """Extract the development log for one or all hypotheses."""
        pattern = f"[{hypothesis_id}]" if hypothesis_id else "[H-"
        log = self._git("log", "--oneline", "--all",
                        f"--grep={pattern}")
        return log


# Recommended repository structure
REPO_STRUCTURE = """
paper-recommender/
    hypotheses.json              # Hypothesis registry (committed to Git)
    pyproject.toml               # Project metadata and dependencies
    src/
        paper_recommender/
            __init__.py
            retrievers.py        # BM25, SPECTER2 retriever implementations
            index.py             # FAISS index management
            evaluation.py        # nDCG, recall, latency benchmarks
            analytics.py         # User engagement tracking
    tests/
        test_hypotheses.py       # Skeleton tests encoding success criteria
        test_unit.py             # Standard unit tests
        conftest.py              # Shared fixtures
    experiments/
        h002_specter_vs_bm25/    # Data and notebooks for each experiment
            benchmark_results.json
            analysis.ipynb
        h003_faiss_latency/
            latency_profile.json
    .github/
        workflows/
            hypothesis_ci.yml    # CI that runs hypothesis tests on push
        ISSUE_TEMPLATE/
            hypothesis.md        # GitHub Issue template for new hypotheses
    docs/
        decisions/               # Architecture Decision Records
            001-embedding-choice.md
"""

print(REPO_STRUCTURE)
Listing 7.15: Git workflow manager with branch naming conventions (experiment/, feature/), structured commit messages ([H-XXX] evidence/confirmed/refuted), and a recommended repository layout for hypothesis-driven projects.

Notice the branch naming convention: when conclude_experiment confirms a hypothesis, it merges the experiment branch into main. At that point the confirmed hypothesis becomes a user story for production hardening. The team creates a feature/ branch from the merged code, writes conventional unit and integration tests around it, adds documentation, and treats the confirmed behavior as a committed product requirement rather than an open question. This is the step where discovery hands off to delivery: the hypothesis provided the evidence that the feature is worth building, and the feature branch provides the engineering rigor to build it well.

7. Putting It All Together: The CI Pipeline

A well-organized repository captures the structure of the experiment, but without automation the team must remember to run tests manually after every change, and manual discipline erodes within weeks.

The final piece is a continuous integration pipeline that runs the hypothesis tests on every push. This is the automated falsification engine: whenever a developer pushes code to an experiment branch, CI runs the relevant skeleton tests and reports whether the success criteria are met. In short: the repository that tests its own beliefs, not just its own code, is the one that finds the wrong turn before the team has driven a hundred miles down it.

"""
GitHub Actions workflow for hypothesis-driven CI.

File: .github/workflows/hypothesis_ci.yml (shown as Python string for syntax highlighting)

This workflow:
1. Runs all unskipped hypothesis tests on every push
2. Updates the hypothesis registry with test results
3. Posts a comment on the PR with the hypothesis status dashboard
"""

HYPOTHESIS_CI_YAML = """
name: Hypothesis CI

on:
  push:
    branches: ['experiment/**']
  pull_request:
    branches: [main]

jobs:
  test-hypotheses:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install -e ".[test]"

      - name: Run hypothesis tests
        run: |
          pytest tests/test_hypotheses.py -v \\
            --tb=short \\
            --junitxml=hypothesis-results.xml \\
            -k "not skip"

      - name: Generate hypothesis dashboard
        if: always()
        run: |
          python -c "
          from paper_recommender.registry import HypothesisRegistry
          from pathlib import Path
          registry = HypothesisRegistry.load(Path('hypotheses.json'))
          print(registry.dashboard())
          "

      - name: Comment PR with hypothesis status
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const dashboard = fs.readFileSync('hypothesis-dashboard.txt', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '## Hypothesis Dashboard\\n```\\n' + dashboard + '\\n```'
            });
"""

print(HYPOTHESIS_CI_YAML)
Listing 7.16: GitHub Actions CI workflow triggered on experiment branches, running unskipped hypothesis tests, generating a status dashboard, and posting results as a pull request comment.

Real-World Application: Spotify's Controlled Experimentation Platform

Spotify reportedly runs thousands of concurrent A/B experiments through its internal platform (as described in their 2020 engineering blog series on experimentation). Each new feature typically starts as a falsifiable hypothesis with a metric threshold, is deployed behind a feature flag to a randomized user subset, and is auto-resolved (ship, iterate, or kill) based on statistical significance against the predefined criterion. The hypothesis registry pattern from this section mirrors that workflow at a smaller scale: branch per experiment, skeleton test per success criterion, CI as the automated falsification engine.

Research Frontier: Autonomous Coding Agents and Hypothesis-Level Reasoning (2024 to 2025)

The SWE-agent system (Yang et al., 2024, "SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering," Princeton/Stanford) demonstrated that a large language model (LLM) agent equipped with a code editor, terminal, and search tools can autonomously resolve 12.5% of real GitHub issues from the SWE-bench benchmark, a task that requires reading issue descriptions, localizing bugs, and writing verified patches. More recent successors such as OpenAI's SWE-bench Verified leaderboard entrants (early 2025) push resolution rates above 40% (as of mid-2026, frontier agents regularly exceed 60% on SWE-bench Verified, with several commercial and open-source systems competing at that level). These systems operate at the level of individual bug fixes, but the trajectory points toward agents that can execute entire hypothesis experiments: given a falsifiable claim and a success criterion, an agent could write the skeleton test, implement candidate solutions, run benchmarks, update the hypothesis registry, and report whether the hypothesis survived falsification. Early prototypes of this experiment-level autonomy appear in the agent-based development systems discussed in Chapter 17: Multi-Agent Software Teams and Chapter 24: Autonomous Software Organizations.

8. Extending the Discovery Workbench

The hypothesis registry and Git workflow from this section become a permanent component of the Discovery Workbench, the platform that grows throughout this book. Specifically, we add a DevTracker module that integrates with the Workbench's existing architecture from Chapter 6.

"""
Discovery Workbench extension: DevTracker module.

Integrates hypothesis-driven development tracking into the
Discovery Workbench platform. This module will be extended in
subsequent chapters with AI-assisted hypothesis generation (Ch 8),
vibe coding integration (Ch 9), and automated experiment design (Ch 46).
"""

from dataclasses import dataclass, field
from typing import List, Dict, Optional
from pathlib import Path
import json


@dataclass
class DevTracker:
    """Development hypothesis tracker for the Discovery Workbench.

    Manages the lifecycle of development hypotheses, from formulation
    through testing to resolution. Connects to the Workbench's
    experiment registry and knowledge graph components.
    """
    project_name: str
    registry_path: Path
    _registry: Optional[object] = field(default=None, repr=False)

    def initialize(self, idea: str) -> Dict:
        """Start a new project from a vague idea.

        Returns a structured decomposition ready for hypothesis formulation.
        This is Stage 1 of the five-stage pipeline.
        """
        return {
            "project": self.project_name,
            "original_idea": idea,
            "uncertainty_categories": {
                "user_needs": [],
                "technical": [],
                "value": [],
                "integration": [],
            },
            "status": "decomposition_needed"
        }

    def compute_sprint_plan(self, budget_hours: float = 80) -> List[Dict]:
        """Generate a prioritized sprint plan from active hypotheses.

        Allocates the sprint budget across hypothesis experiments,
        balancing exploration (testing uncertain hypotheses) with
        exploitation (building on confirmed ones).
        """
        # Load registry and prioritize
        # (Implementation builds on Listings 7.12 and 7.13)
        plan = []
        explore_budget = budget_hours * 0.35  # 35% starting ratio; adjust per team maturity
        exploit_budget = budget_hours * 0.65

        # Exploration: test highest-information-value hypotheses
        plan.append({
            "phase": "explore",
            "budget_hours": explore_budget,
            "goal": "Test top-priority untested hypotheses",
            "hypotheses": []  # Populated from priority queue
        })

        # Exploitation: build features from confirmed hypotheses
        plan.append({
            "phase": "exploit",
            "budget_hours": exploit_budget,
            "goal": "Implement features from confirmed hypotheses",
            "hypotheses": []  # Populated from confirmed queue
        })

        return plan

    def generate_report(self) -> str:
        """Generate a discovery progress report for the sprint retrospective."""
        return (
            f"Discovery Report: {self.project_name}\n"
            f"Registry: {self.registry_path}\n"
            f"Status: Active\n"
            # Full implementation reads from the hypothesis registry
            # and computes discovery velocity, entropy reduction, etc.
        )
Listing 7.17: DevTracker module for the Discovery Workbench, providing project initialization from a vague idea, sprint planning with a 35/65 explore/exploit budget split, and discovery progress reporting.
Library Shortcut: Existing Tools for Hypothesis-Driven Development

The full workflow we built from scratch (roughly 400 lines across Listings 7.11 through 7.17) can be approximated with existing tools. pytest with markers and parametrize handles the skeleton test framework in about 50 lines. GitHub Issues with custom templates replaces the hypothesis registry. LaunchDarkly or Statsig provides production A/B testing with statistical analysis. MLflow tracks experiment results. The from-scratch implementation is valuable for understanding the concepts, but a production team would compose these existing tools, reducing setup from an afternoon to roughly 30 minutes of configuration. The DevTracker module in the Discovery Workbench uses this composable approach internally.

Try It: Build a Hypothesis Registry in 30 Minutes

Pick any small project idea you have been considering (a personal budgeting tracker, a recipe search tool, a reading list organizer) and build a working hypothesis registry from scratch.

  1. Create the repository. Run mkdir my-discovery-project && cd my-discovery-project && git init. Create a hypotheses.json file containing an empty list: {"project": "my-project", "hypotheses": []}. Commit it to main.
  2. Decompose your idea. In a Python script called decompose.py, use the IdeaDecomposition pattern from Listing 7.11 to list at least four uncertainties across at least two categories. Run the script and save the printed summary to docs/decomposition.txt.
  3. Formulate two hypotheses. Pick the two highest-stakes uncertainties and write them as DevHypothesis entries (Listing 7.12 pattern). Add them to hypotheses.json with falsifiable claims, concrete test procedures, and numerical success criteria. Commit.
  4. Write skeleton tests. Create tests/test_hypotheses.py with one pytest.mark.skip-decorated test per hypothesis. Each test should assert against the success criterion threshold. Run pytest --collect-only to verify both tests are discovered and reported as skipped.
  5. Branch and record evidence. Create an experiment branch (git checkout -b experiment/h-001), remove the skip marker from one test, make it pass with a stub implementation, and commit with the message format [H-001] evidence (supports): stub passes threshold. Run git log --oneline --all to see your experiment log taking shape.

Exercise 7.3.1

The hypothesis H-003 ("ANN search achieves p95 < 200ms on 50M papers") depends on H-002. Suppose H-002 is refuted: SPECTER2 does not beat BM25. Should H-003 still be tested? Explain your reasoning in terms of the dependency graph, and describe what change (if any) you would make to H-003's claim, test procedure, and success criterion before proceeding.

Hint

Consider whether the latency question disappears entirely or merely changes form. BM25 retrieval uses inverted indices rather than dense vectors, so the performance profile is completely different. Think about what "depends_on" means: does refuting the parent make the child irrelevant, or does it require reformulating the child against the new technical direction?

Lab: Hypothesis Priority Sensitivity Analysis

Goal: Discover how the three weighting factors (uncertainty, fan-out, category weight) each influence the final priority ranking, and identify which factor dominates under realistic conditions.

Tools: Python 3.10+, NumPy, matplotlib.

Procedure (20 minutes): Copy the information_value function from Listing 7.13 and the five hypotheses from Listing 7.12 into a standalone script. Sweep each factor independently: (1) vary confidence from 0.1 to 0.9 in steps of 0.05 while holding fan-out and category weight constant; (2) vary the number of dependents from 0 to 6; (3) swap category weights between hypotheses. For each sweep, plot the resulting priority ranking as a heatmap or line chart.

What to observe: At what confidence level does a "value" hypothesis drop below an "implementation" hypothesis with three dependents? Is there a fan-out count that always dominates regardless of category? How sensitive is the final ranking to small changes in the category weight constants (try 1.2 vs. 1.5 for "requirement")? Record the crossover points and reflect on whether the default weights match your intuition about what should be tested first.

Exercises

  1. (Conceptual) The hypothesis registry in Listing 7.12 uses a simple Bayesian update rule. Describe two limitations of this approach compared to a full Bayesian model with proper likelihood functions. How would you design a more rigorous confidence update that accounts for different types of evidence (user interviews vs. A/B tests vs. offline benchmarks)?
  2. (Coding) Implement the complete five-stage pipeline (shown in Figure 7.3) for a project of your choice. Start with a vague idea, decompose it into at least six uncertainties, formulate hypotheses, prioritize them, and write skeleton pytest tests. Push the result to a Git repository with proper branching. Run pytest --co (collect only) to verify that all tests are discovered and properly skipped.
  3. (Analysis) The prioritization function in Listing 7.13 uses a multiplicative combination of uncertainty, fan-out, and category weight. Design an experiment to test whether this formula produces better outcomes than a simpler "highest uncertainty first" rule. Define your success criterion, describe the simulation setup, and predict which approach will win under high-uncertainty vs. low-uncertainty conditions.

What's Next

You now have a complete framework for treating software development as a discovery process, from theory (Section 7.1) through AI acceleration (Section 7.2) to a hands-on workflow (this section). In Chapter 8: Foundations of AI Assisted Software Engineering, we dive into the AI tools themselves: how large language models generate code, how code completion engines learn from context, and how to evaluate whether an AI assistant is actually helping you discover the right software faster. The hypothesis-driven framework from this chapter provides the evaluation lens: the best AI coding tool is the one that maximizes your team's discovery velocity.

Bibliography

pytest documentation

The testing framework used throughout this section for encoding hypothesis success criteria as executable tests.

Chacon, S. & Straub, B. (2014). Pro Git, 2nd edition. Apress.

The definitive reference for Git workflows, branching strategies, and repository management.

Olsson, H. H. & Bosch, J. (2017). From opinions to data-driven software R&D: a multi-case study on how to close the "open loop" problem. IEEE Software, 34(5), 51-57.

Empirical study of hypothesis-driven development in industry, supporting the framework presented in this chapter.

MLflow

Open-source experiment tracking platform referenced in the library shortcut as an alternative to the from-scratch registry.

Jimenez, C. E., et al. (2024). SWE-bench: Can language models resolve real-world GitHub issues? ICLR 2024.

Benchmark for evaluating AI agents on software development tasks, relevant to the research frontier discussion.