Skip to content

Commit 933ffcc

Browse files
authored
feat(prd): recursive decomposition stress test (#421)
## Summary - New `cf prd stress-test` command with recursive PRD decomposition - Tri-state classification: atomic / composite / ambiguous - Ambiguity report surfaces requirements gaps with actionable questions - Technical specification generated from decomposition tree - Interactive mode resolves ambiguities and creates new PRD version - 23 new tests, all 1882 existing tests still pass ## Validation - Review feedback: 8 items addressed across 3 rounds - Demo: All 8 acceptance criteria verified - Tests: 23/23 passing + 1882 existing - CI: All checks green - Linting: Clean Closes #421
1 parent 1d3f2d5 commit 933ffcc

3 files changed

Lines changed: 1000 additions & 0 deletions

File tree

codeframe/cli/app.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,6 +1590,167 @@ def prd_generate(
15901590
raise typer.Exit(1)
15911591

15921592

1593+
@prd_app.command("stress-test")
1594+
def prd_stress_test(
1595+
repo_path: Optional[Path] = typer.Option(
1596+
None,
1597+
"--workspace", "-w",
1598+
help="Workspace path (defaults to current directory)",
1599+
),
1600+
prd_id: Optional[str] = typer.Option(
1601+
None,
1602+
"--prd-id",
1603+
help="Specific PRD ID (defaults to latest)",
1604+
),
1605+
max_depth: int = typer.Option(
1606+
3,
1607+
"--max-depth",
1608+
min=1,
1609+
max=10,
1610+
help="Maximum recursion depth (default: 3, range: 1-10)",
1611+
),
1612+
output: Optional[Path] = typer.Option(
1613+
None,
1614+
"--output", "-o",
1615+
help="Write tech spec to this file",
1616+
),
1617+
interactive: bool = typer.Option(
1618+
False,
1619+
"--interactive", "-i",
1620+
help="Resolve ambiguities inline and update PRD",
1621+
),
1622+
) -> None:
1623+
"""Stress-test a PRD via recursive decomposition.
1624+
1625+
Recursively decomposes PRD goals using a tri-state classification
1626+
(atomic / composite / ambiguous) to surface requirements gaps and
1627+
generate a technical specification.
1628+
1629+
Produces two outputs:
1630+
1. Ambiguity Report — questions the PRD doesn't answer
1631+
2. Technical Specification — decomposition tree as markdown
1632+
1633+
Use --interactive to resolve ambiguities inline and update the PRD.
1634+
1635+
Requires ANTHROPIC_API_KEY environment variable.
1636+
1637+
Example:
1638+
codeframe prd stress-test
1639+
codeframe prd stress-test --max-depth 4
1640+
codeframe prd stress-test --output spec.md
1641+
codeframe prd stress-test --interactive
1642+
"""
1643+
import os
1644+
from codeframe.core.workspace import get_workspace
1645+
from codeframe.core import prd as prd_module
1646+
from codeframe.adapters.llm.anthropic import AnthropicProvider
1647+
from codeframe.core.prd_stress_test import (
1648+
stress_test_prd,
1649+
resolve_ambiguities_into_prd,
1650+
)
1651+
from codeframe.core.events import emit_for_workspace, EventType
1652+
from rich.panel import Panel
1653+
1654+
workspace_path = repo_path or Path.cwd()
1655+
1656+
try:
1657+
workspace = get_workspace(workspace_path)
1658+
except Exception as e:
1659+
console.print(f"[red]Error:[/red] {e}")
1660+
raise typer.Exit(1)
1661+
1662+
# Load PRD
1663+
if prd_id:
1664+
record = prd_module.get_by_id(workspace, prd_id)
1665+
else:
1666+
record = prd_module.get_latest(workspace)
1667+
1668+
if not record:
1669+
console.print("[red]Error:[/red] No PRD found. Run 'codeframe prd generate' first.")
1670+
raise typer.Exit(1)
1671+
1672+
console.print(f"[dim]Stress-testing PRD: {record.title} (v{record.version})[/dim]")
1673+
1674+
# Build provider
1675+
api_key = os.getenv("ANTHROPIC_API_KEY")
1676+
if not api_key:
1677+
console.print("[red]Error:[/red] ANTHROPIC_API_KEY environment variable required.")
1678+
raise typer.Exit(1)
1679+
1680+
provider = AnthropicProvider(api_key=api_key)
1681+
1682+
# Run stress test
1683+
console.print(f"[dim]Recursively decomposing (max depth: {max_depth})...[/dim]")
1684+
result = stress_test_prd(record.content, provider, max_depth=max_depth)
1685+
1686+
# Show ambiguity report
1687+
if result.ambiguities:
1688+
console.print(f"\n[bold yellow]⚠ {len(result.ambiguities)} ambiguities found[/bold yellow]\n")
1689+
for i, amb in enumerate(result.ambiguities, 1):
1690+
console.print(f"[bold yellow]{i}. {amb.label}[/bold yellow] (from \"{amb.source_node_title}\")")
1691+
console.print(" The PRD doesn't specify:")
1692+
for q in amb.questions:
1693+
console.print(f" [cyan]- {q}[/cyan]")
1694+
console.print(f" → {amb.recommendation}")
1695+
console.print()
1696+
else:
1697+
console.print("\n[green]✓ No ambiguities found — PRD is well-specified.[/green]\n")
1698+
1699+
# Interactive resolution
1700+
if interactive and result.ambiguities:
1701+
console.print("[bold]Interactive mode — resolve ambiguities:[/bold]\n")
1702+
for amb in result.ambiguities:
1703+
console.print(f"[yellow]{amb.label}[/yellow]: {', '.join(amb.questions)}")
1704+
answer = typer.prompt("Your answer")
1705+
amb.resolved_answer = answer
1706+
console.print("[green]✓[/green] Recorded.\n")
1707+
1708+
# Update PRD with resolved answers
1709+
console.print("[dim]Updating PRD with resolved ambiguities...[/dim]")
1710+
updated_content = resolve_ambiguities_into_prd(
1711+
record.content, result.ambiguities, provider,
1712+
)
1713+
new_record = prd_module.create_new_version(
1714+
workspace, record.id, updated_content,
1715+
f"Stress-test: resolved {len([a for a in result.ambiguities if a.resolved_answer])} ambiguities",
1716+
)
1717+
if new_record:
1718+
console.print(f"[green]✓[/green] PRD updated to version {new_record.version}")
1719+
emit_for_workspace(
1720+
workspace, EventType.PRD_UPDATED,
1721+
{"prd_id": new_record.id, "source": "stress_test_resolution"},
1722+
print_event=False,
1723+
)
1724+
# Re-run stress test on updated PRD to reflect resolved ambiguities
1725+
console.print("[dim]Re-analyzing updated PRD...[/dim]")
1726+
result = stress_test_prd(new_record.content, provider, max_depth=max_depth)
1727+
else:
1728+
console.print("[yellow]Warning:[/yellow] Failed to create new PRD version.")
1729+
1730+
# Show tech spec
1731+
console.print(Panel(result.tech_spec_markdown[:2000], title="Technical Specification", border_style="blue"))
1732+
1733+
# Write to file
1734+
if output:
1735+
output.write_text(result.tech_spec_markdown)
1736+
console.print(f"\n[green]✓[/green] Tech spec written to [bold]{output}[/bold]")
1737+
1738+
# Summary
1739+
node_count = _count_nodes(result.tree)
1740+
console.print(f"\n[bold]Summary:[/bold] {len(result.tree)} goals, {node_count} nodes, {len(result.ambiguities)} ambiguities")
1741+
if result.ambiguities and not interactive:
1742+
console.print("[dim]Tip: Run with --interactive to resolve ambiguities and update the PRD[/dim]")
1743+
1744+
1745+
def _count_nodes(tree: list) -> int:
1746+
"""Count total nodes in the decomposition tree."""
1747+
count = 0
1748+
for node in tree:
1749+
count += 1
1750+
count += _count_nodes(node.children)
1751+
return count
1752+
1753+
15931754
# Tasks commands
15941755
tasks_app = typer.Typer(
15951756
name="tasks",

0 commit comments

Comments
 (0)