# How Ouroboros Performs Hierarchical Acceptance Criteria Decomposition

> Learn how Ouroboros performs hierarchical acceptance criteria decomposition. Discover its deterministic LLM-augmented pipeline for MECE principles and dependency mapping on Q00/ouroboros.

- Repository: [Q00/ouroboros](https://github.com/Q00/ouroboros)
- Tags: deep-dive
- Published: 2026-03-14

---

**Ouroboros converts a single high-level Acceptance Criterion into a balanced, multi-level tree through a deterministic, LLM-augmented pipeline that enforces MECE principles and explicit dependency mapping.**

Hierarchical Acceptance Criteria decomposition is the core mechanism that allows the Ouroboros framework to break down vague project goals into executable, atomic tasks. According to the Q00/ouroboros source code, this process operates as an immutable pipeline tightly coupled to the Double-Diamond execution phase, transforming each parent criterion into 2–5 validated child nodes with tracked dependencies.

## The AC Tree Foundation

Before decomposition begins, Ouroboros establishes an immutable data structure to maintain hierarchical integrity throughout execution.

### ACTree and ACNode Architecture

The **AC tree** (`ACTree`) serves as the central registry for all criteria, with each criterion stored as an `ACNode` in [`src/ouroboros/core/ac_tree.py`](https://github.com/Q00/ouroboros/blob/main/src/ouroboros/core/ac_tree.py). Every node records:

- **Depth** and **parent** references
- **Children** IDs and **status** flags
- **Atomic** flags indicating terminal nodes
- **Execution** metadata

The tree enforces a **hard depth limit of 5** (NFR 10) and provides helper methods like `can_decompose` and `is_cyclic` to prevent invalid tree states.

## The Decomposition Pipeline

When the executor encounters a non-atomic AC, it triggers the decomposition workflow defined in [`src/ouroboros/execution/decomposition.py`](https://github.com/Q00/ouroboros/blob/main/src/ouroboros/execution/decomposition.py).

### Step 1: Eligibility Verification

The executor first checks `ACTree.can_decompose(ac_id)` to validate that:

- The AC exists in the tree
- Current depth is less than the maximum depth limit
- Status is not already `DECOMPOSED` or `COMPLETED`
- The node is not marked as `is_atomic`

Only nodes passing all four conditions proceed to decomposition.

### Step 2: Context Compression

For deep hierarchies (depth ≥ 3), Ouroboros applies `_compress_context` to truncate *Discover* phase insights to the first 500 characters. This optimization prevents LLM prompt bloat while preserving essential context for child generation.

### Step 3: LLM-Driven Child Generation

The `decompose_ac` function constructs a system prompt (`DECOMPOSITION_SYSTEM_PROMPT`) that forces the model to obey **MECE** (Mutually Exclusive, Collectively Exhaustive) principles. The prompt mandates:

- **2–5 child ACs** per parent
- Explicit **dependency declarations** between siblings
- Complete coverage of the parent scope without overlap

The user prompt injects the parent AC content, compressed discovery insights, and current depth metadata.

### Step 4: Response Parsing and Validation

Raw LLM output passes through `_extract_json_from_response`, which tolerates plain JSON, markdown code blocks, or loosely structured snippets. The parser accepts both legacy formats (plain strings) and current formats (dictionaries with `"content"` and `"depends_on"` fields).

For each child, the system builds:
- **Content**: The AC text
- **Dependencies**: A tuple of valid predecessor indices (only indices less than the child's own index are retained; forward or self-references are filtered and logged)

The `_validate_children` function enforces strict cardinality (2–5 children), prohibits cyclic references (child content cannot equal parent), and rejects empty strings. Violations return a `DecompositionError` and emit an `ac_decomposition_failed` event.

### Step 5: Tree Integration

Upon validation success, the system mints fresh identifiers (`ac_{uuid}`), assembles a `DecompositionResult`, and emits an `ac_decomposed` success event. The orchestrator adds each new `ACNode` to the `ACTree` via `ACTree.add_node`, updates the parent node's `children_ids`, and sets the parent status to `DECOMPOSED`.

## Practical Implementation Examples

### Programmatic Decomposition

```python
import asyncio
from ouroboros.execution.decomposition import decompose_ac
from ouroboros.providers.litellm_adapter import LiteLLMAdapter

async def demo():
    # LLM adapter (already configured via env or .env.example)

    llm = LiteLLMAdapter()

    result = await decompose_ac(
        ac_content="Provide a secure, multi‑tenant authentication system",
        ac_id="ac_root",
        execution_id="exec_001",
        depth=0,
        llm_adapter=llm,
        discover_insights="Users must be able to register, login, reset passwords, and use MFA.",
    )

    if result.is_ok:
        decomp = result.value
        print("✅ Decomposition succeeded")
        for i, (cid, txt) in enumerate(zip(decomp.child_ac_ids, decomp.child_acs)):
            deps = decomp.dependencies[i]
            print(f"Child {i}: {cid}")
            print(f"  Content   : {txt}")
            print(f"  Depends on: {deps or 'none'}")
    else:
        print("❌ Decomposition failed:", result.error)

asyncio.run(demo())

```

### Integrating with the ACTree

```python
from ouroboros.core.ac_tree import ACTree, ACNode

tree = ACTree()

# Root node (usually created from the Seed)

root = ACNode.create(content="Implement authentication", depth=0)
tree.add_node(root)

# Suppose decompose_ac returned `decomp` from the example above

for child_id, child_content in zip(decomp.child_ac_ids, decomp.child_acs):
    child_node = ACNode.create(
        content=child_content,
        depth=root.depth + 1,
        parent_id=root.id,
    )
    # Preserve the generated ID from the decomposition result

    child_node = ACNode(
        id=child_id,
        content=child_node.content,
        depth=child_node.depth,
        parent_id=child_node.parent_id,
        status=child_node.status,
        is_atomic=False,
        children_ids=tuple(),
        execution_id=None,
        metadata={},
    )
    tree.add_node(child_node)

# Update the root to point at its children

root_with_children = root.with_children(tuple(decomp.child_ac_ids))
tree.update_node(root_with_children)

print(f"Tree now has {len(tree.nodes)} nodes; depth of deepest leaf: {max(n.depth for n in tree.nodes.values())}")

```

### Parallel Execution Based on Dependencies

```python
from ouroboros.execution.parallel import ParallelACExecutor

executor = ParallelACExecutor(tree=tree, execution_id="exec_001")
await executor.run_ac(root.id)   # The executor respects the dependency matrix stored in each node

```

## Key Source Files

| File | Role |
|------|------|
| **src/ouroboros/core/ac_tree.py** | Immutable AC node definition, tree container, depth limits, and decomposition eligibility checks. |
| **src/ouroboros/execution/decomposition.py** | LLM-driven hierarchical decomposition, context compression, validation, and dependency parsing. |
| **src/ouroboros/events/decomposition.py** | Event factories `create_ac_decomposed_event` and `create_ac_decomposition_failed_event` for audit logging. |
| **docs/api/parallel-execution.md** | Documentation for scheduling child ACs in parallel while respecting dependency constraints. |

## Summary

- **Hierarchical Acceptance Criteria decomposition** in Ouroboros transforms high-level goals into executable trees using a deterministic, LLM-augmented pipeline.
- The **ACTree** enforces a hard depth limit of 5 and tracks parent-child relationships through immutable **ACNode** objects.
- Decomposition triggers only when `can_decompose` validates depth limits, non-atomic status, and absence of cyclic references.
- The **MECE principle** governs LLM prompts, requiring 2–5 mutually exclusive, collectively exhaustive children with explicit dependency declarations.
- **Context compression** truncates discovery insights to 500 characters for deep hierarchies (depth ≥ 3) to optimize token usage.
- Validation ensures cardinality constraints, non-empty content, and valid dependency indices before immutable tree updates.

## Frequently Asked Questions

### What triggers Hierarchical Acceptance Criteria decomposition in Ouroboros?

Decomposition triggers when the executor encounters an AC that passes the `can_decompose` check in [`src/ouroboros/core/ac_tree.py`](https://github.com/Q00/ouroboros/blob/main/src/ouroboros/core/ac_tree.py). The criterion must exist in the tree, have a depth below the maximum limit of 5, not be marked as atomic, and not already hold a status of `DECOMPOSED` or `COMPLETED`.

### How does Ouroboros prevent infinite decomposition loops?

The system enforces a **hard depth limit of 5** (NFR 10) through the `ACTree` class, which rejects any decomposition attempt that would exceed this boundary. Additionally, the `_validate_children` function in [`src/ouroboros/execution/decomposition.py`](https://github.com/Q00/ouroboros/blob/main/src/ouroboros/execution/decomposition.py) explicitly checks for cyclic references by ensuring child content does not equal parent content.

### What is the MECE principle and how is it enforced?

**MECE** stands for Mutually Exclusive, Collectively Exhaustive. During decomposition, the system prompt (`DECOMPOSITION_SYSTEM_PROMPT`) instructs the LLM to generate child ACs that do not overlap with each other (mutually exclusive) while completely covering the parent scope (collectively exhaustive). The prompt also mandates that the model produce between 2 and 5 children and explicitly declare dependencies between siblings.

### How does Ouroboros handle dependencies between decomposed criteria?

During the parsing phase in [`src/ouroboros/execution/decomposition.py`](https://github.com/Q00/ouroboros/blob/main/src/ouroboros/execution/decomposition.py), the system extracts dependency declarations from the LLM response and converts them into tuples of valid predecessor indices. The system filters out forward references and self-references, keeping only indices less than the child's own position. These dependency tuples are stored in the `DecompositionResult` and used by the `ParallelACExecutor` to determine which child ACs can run concurrently versus which must wait for predecessors to complete.