Files
pm-claude-skills/plugins/pm-planning/skills/rice-prioritisation/scripts/rice_calculator.py
T
Claude 760f979365 Add cross-tool positioning, Python helpers, tiers, and hygiene docs
Five improvements to position the library as a serious engineering project:

1. Cross-tool compatibility — new README "Works With" section honestly
   documenting where skills run (Claude Code natively; SKILL.md bodies
   port to other agents and chat LLMs as system prompts).

2. Python helper scripts (stdlib-only) for the three strongest skills:
   - sprint-planning: capacity_calculator.py (recommended commitment)
   - rice-prioritisation: rice_calculator.py (ranks, flags quick wins/moonshots)
   - cs-health-scorecard: health_score.py (weighted total + RAG)
   Each is wired into its SKILL.md and synced to the plugin copies.

3. Explicit skill tiering — TIERS.md + README section marking 46
   Production-Ready skills and calling out Experimental (external-dependency)
   ones; everything else is Stable.

4. Repository hygiene — new CHANGELOG.md (Keep a Changelog format) and
   SKILL-AUTHORING-STANDARD.md; refreshed SECURITY.md version table and
   helper-script disclosure; added .gitignore.

5. Related Projects — README section linking to alirezarezvani/claude-skills
   and the major awesome-claude-skills / awesome-claude-code lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px
2026-06-17 07:48:48 +00:00

171 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""RICE score calculator for the rice-prioritisation skill.
Computes RICE = (Reach × Impact × Confidence) / Effort for a list of
initiatives, ranks them, and flags quick wins and moonshots so the ranking in a
prioritisation doc is calculated consistently rather than eyeballed. Pure Python
standard library — no dependencies, no network access.
Input
-----
A JSON or CSV list of initiatives. Each needs: name, reach, impact, confidence,
effort.
- impact uses the standard RICE scale (3, 2, 1, 0.5, 0.25) but any number works.
- confidence is a fraction (0.8) or a percentage (80) — both are accepted.
- effort is in person-months and must be > 0.
JSON example (rice.json):
[
{"name": "Onboarding redesign", "reach": 5000, "impact": 2, "confidence": 0.8, "effort": 3},
{"name": "Dark mode", "reach": 8000, "impact": 0.5, "confidence": 1.0, "effort": 1}
]
CSV example (header row required):
name,reach,impact,confidence,effort
Onboarding redesign,5000,2,0.8,3
Dark mode,8000,0.5,1.0,1
Usage
-----
python3 rice_calculator.py rice.json
python3 rice_calculator.py rice.csv --format csv
cat rice.json | python3 rice_calculator.py - --json
"""
from __future__ import annotations
import argparse
import csv
import io
import json
import sys
from dataclasses import dataclass
@dataclass
class Initiative:
name: str
reach: float
impact: float
confidence: float
effort: float
@property
def score(self) -> float:
if self.effort <= 0:
raise ValueError(f"Effort for '{self.name}' must be greater than 0.")
return (self.reach * self.impact * self.confidence) / self.effort
def _normalise_confidence(value: float) -> float:
"""Accept 80 or 0.8; return a fraction between 0 and 1."""
return value / 100.0 if value > 1 else value
def _to_initiative(row: dict) -> Initiative:
try:
return Initiative(
name=str(row["name"]).strip(),
reach=float(row["reach"]),
impact=float(row["impact"]),
confidence=_normalise_confidence(float(row["confidence"])),
effort=float(row["effort"]),
)
except KeyError as exc:
raise ValueError(f"Missing required field {exc} in row: {row}") from None
def load(text: str, fmt: str) -> list[Initiative]:
if fmt == "csv":
rows = list(csv.DictReader(io.StringIO(text)))
else:
rows = json.loads(text)
if not isinstance(rows, list):
raise ValueError("Input must be a list of initiatives.")
return [_to_initiative(r) for r in rows]
def rank(initiatives: list[Initiative]) -> list[dict]:
scored = []
for i in initiatives:
scored.append({
"name": i.name,
"reach": i.reach,
"impact": i.impact,
"confidence": round(i.confidence, 2),
"effort": i.effort,
"rice_score": round(i.score, 1),
})
scored.sort(key=lambda d: d["rice_score"], reverse=True)
if scored:
max_score = max(d["rice_score"] for d in scored) or 1
max_effort = max(d["effort"] for d in scored) or 1
for rank_index, d in enumerate(scored, start=1):
d["rank"] = rank_index
flags = []
# Quick win: strong score relative to the field, low relative effort.
if d["rice_score"] >= 0.5 * max_score and d["effort"] <= 0.33 * max_effort:
flags.append("quick-win")
# Moonshot: high raw impact, high relative effort.
if d["impact"] >= 2 and d["effort"] >= 0.66 * max_effort:
flags.append("moonshot")
# Low-confidence estimates should be revisited before acting.
if d["confidence"] <= 0.5:
flags.append("low-confidence")
d["flags"] = flags
return scored
def _render(scored: list[dict]) -> str:
header = f"{'#':>2} {'Initiative':<32} {'Reach':>8} {'Imp':>4} {'Conf':>5} {'Eff':>5} {'RICE':>8} Flags"
lines = ["RICE Prioritisation", "=" * len(header), header, "-" * len(header)]
for d in scored:
lines.append(
f"{d['rank']:>2} {d['name'][:32]:<32} {d['reach']:>8g} {d['impact']:>4g} "
f"{d['confidence']:>5.2f} {d['effort']:>5g} {d['rice_score']:>8g} {', '.join(d['flags'])}"
)
quick = [d["name"] for d in scored if "quick-win" in d["flags"]]
moon = [d["name"] for d in scored if "moonshot" in d["flags"]]
lowc = [d["name"] for d in scored if "low-confidence" in d["flags"]]
lines.append("")
lines.append(f"Quick wins (do alongside bigger bets): {', '.join(quick) or 'none'}")
lines.append(f"Moonshots (high impact, high effort): {', '.join(moon) or 'none'}")
lines.append(f"Low confidence — revisit estimates: {', '.join(lowc) or 'none'}")
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("input", help="Path to a JSON/CSV file of initiatives, or '-' for stdin.")
parser.add_argument("--format", choices=["json", "csv"], help="Input format (inferred from extension if omitted).")
parser.add_argument("--json", action="store_true", dest="as_json", help="Emit ranked JSON instead of a table.")
args = parser.parse_args(argv)
text = sys.stdin.read() if args.input == "-" else None
fmt = args.format
if text is None:
try:
text = open(args.input).read()
except OSError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if fmt is None:
fmt = "csv" if args.input.lower().endswith(".csv") else "json"
fmt = fmt or "json"
try:
scored = rank(load(text, fmt))
except (ValueError, json.JSONDecodeError) as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
print(json.dumps(scored, indent=2) if args.as_json else _render(scored))
return 0
if __name__ == "__main__":
raise SystemExit(main())