124 lines
4.0 KiB
Python
Executable File
124 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Prioritize product assumptions and suggest validation tests."""
|
|
|
|
import argparse
|
|
import csv
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class Assumption:
|
|
statement: str
|
|
category: str
|
|
risk: float
|
|
certainty: float
|
|
|
|
@property
|
|
def priority_score(self) -> float:
|
|
# High-risk, low-certainty assumptions should be tested first.
|
|
return self.risk * (1.0 - self.certainty)
|
|
|
|
|
|
def parse_float(value: str, field: str) -> float:
|
|
number = float(value)
|
|
if number < 0 or number > 1:
|
|
raise ValueError(f"{field} must be in [0, 1]")
|
|
return number
|
|
|
|
|
|
def suggest_test(category: str) -> str:
|
|
category = category.lower().strip()
|
|
if category == "desirability":
|
|
return "problem interviews or fake-door test"
|
|
if category == "viability":
|
|
return "pricing/willingness-to-pay test"
|
|
if category == "feasibility":
|
|
return "technical spike or architecture prototype"
|
|
if category == "usability":
|
|
return "moderated usability test"
|
|
return "smallest possible experiment with clear success criteria"
|
|
|
|
|
|
def load_from_csv(path: str) -> list[Assumption]:
|
|
assumptions: list[Assumption] = []
|
|
with open(path, "r", encoding="utf-8", newline="") as handle:
|
|
reader = csv.DictReader(handle)
|
|
required = {"assumption", "category", "risk", "certainty"}
|
|
missing = required - set(reader.fieldnames or [])
|
|
if missing:
|
|
missing_str = ", ".join(sorted(missing))
|
|
raise ValueError(f"Missing required columns: {missing_str}")
|
|
|
|
for row in reader:
|
|
assumptions.append(
|
|
Assumption(
|
|
statement=(row.get("assumption") or "").strip(),
|
|
category=(row.get("category") or "").strip(),
|
|
risk=parse_float(row.get("risk") or "0", "risk"),
|
|
certainty=parse_float(row.get("certainty") or "0", "certainty"),
|
|
)
|
|
)
|
|
return assumptions
|
|
|
|
|
|
def parse_inline(items: list[str]) -> list[Assumption]:
|
|
assumptions: list[Assumption] = []
|
|
for item in items:
|
|
# format: statement|category|risk|certainty
|
|
parts = [part.strip() for part in item.split("|")]
|
|
if len(parts) != 4:
|
|
raise ValueError("Inline assumption must be: statement|category|risk|certainty")
|
|
assumptions.append(
|
|
Assumption(
|
|
statement=parts[0],
|
|
category=parts[1],
|
|
risk=parse_float(parts[2], "risk"),
|
|
certainty=parse_float(parts[3], "certainty"),
|
|
)
|
|
)
|
|
return assumptions
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Prioritize assumptions and generate test plan.")
|
|
parser.add_argument("input", nargs="?", help="CSV file path")
|
|
parser.add_argument(
|
|
"--assumption",
|
|
action="append",
|
|
default=[],
|
|
help="Inline assumption: statement|category|risk|certainty",
|
|
)
|
|
parser.add_argument("--top", type=int, default=10, help="Maximum assumptions to print")
|
|
return parser
|
|
|
|
|
|
def main() -> int:
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
|
|
assumptions: list[Assumption] = []
|
|
if args.input:
|
|
assumptions.extend(load_from_csv(args.input))
|
|
if args.assumption:
|
|
assumptions.extend(parse_inline(args.assumption))
|
|
|
|
if not assumptions:
|
|
parser.error("Provide a CSV input file or at least one --assumption value.")
|
|
|
|
assumptions.sort(key=lambda item: item.priority_score, reverse=True)
|
|
|
|
print("prioritized_assumption_test_plan")
|
|
print("rank,priority_score,category,risk,certainty,test,assumption")
|
|
for rank, item in enumerate(assumptions[: args.top], start=1):
|
|
test = suggest_test(item.category)
|
|
print(
|
|
f"{rank},{item.priority_score:.4f},{item.category},{item.risk:.2f},"
|
|
f"{item.certainty:.2f},{test},{item.statement}"
|
|
)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|