Reapplies @zeotrix's PR #48 onto current main: - adds a dependency-free Python script computing RICE/ICE rankings so scoring is consistent across sessions (skills/ + plugins/ copies, kept identical) - documents it in a 'Programmatic Helper' section in both SKILL.md files - regenerates the platform exports so the check-generated CI stays green Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: zeotrix <zeotrix@users.noreply.github.com>
This commit is contained in:
@@ -75,6 +75,29 @@ Recommend building: all Basic features first → Performance features for key us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Programmatic Helper
|
||||||
|
|
||||||
|
This skill ships with a stdlib-only Python script that computes ranking for the math-based frameworks (RICE, ICE) so feature scoring is consistent across sessions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.json --framework rice
|
||||||
|
|
||||||
|
# RICE from CSV
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
|
||||||
|
# ICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py features.json --framework ice
|
||||||
|
|
||||||
|
# Pipe into it
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 scripts/feature_prioritisation.py --framework ice -
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--json` to produce machine-readable output for downstream tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
### Feature Prioritisation — [Product/Team] — [Date]
|
### Feature Prioritisation — [Product/Team] — [Date]
|
||||||
|
|||||||
@@ -75,6 +75,29 @@ Recommend building: all Basic features first → Performance features for key us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Programmatic Helper
|
||||||
|
|
||||||
|
This skill ships with a stdlib-only Python script that computes ranking for the math-based frameworks (RICE, ICE) so feature scoring is consistent across sessions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.json --framework rice
|
||||||
|
|
||||||
|
# RICE from CSV
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
|
||||||
|
# ICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py features.json --framework ice
|
||||||
|
|
||||||
|
# Pipe into it
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 scripts/feature_prioritisation.py --framework ice -
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--json` to produce machine-readable output for downstream tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
### Feature Prioritisation — [Product/Team] — [Date]
|
### Feature Prioritisation — [Product/Team] — [Date]
|
||||||
|
|||||||
@@ -81,6 +81,29 @@ Recommend building: all Basic features first → Performance features for key us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Programmatic Helper
|
||||||
|
|
||||||
|
This skill ships with a stdlib-only Python script that computes ranking for the math-based frameworks (RICE, ICE) so feature scoring is consistent across sessions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.json --framework rice
|
||||||
|
|
||||||
|
# RICE from CSV
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
|
||||||
|
# ICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py features.json --framework ice
|
||||||
|
|
||||||
|
# Pipe into it
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 scripts/feature_prioritisation.py --framework ice -
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--json` to produce machine-readable output for downstream tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
### Feature Prioritisation — [Product/Team] — [Date]
|
### Feature Prioritisation — [Product/Team] — [Date]
|
||||||
|
|||||||
@@ -79,6 +79,29 @@ Recommend building: all Basic features first → Performance features for key us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Programmatic Helper
|
||||||
|
|
||||||
|
This skill ships with a stdlib-only Python script that computes ranking for the math-based frameworks (RICE, ICE) so feature scoring is consistent across sessions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.json --framework rice
|
||||||
|
|
||||||
|
# RICE from CSV
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
|
||||||
|
# ICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py features.json --framework ice
|
||||||
|
|
||||||
|
# Pipe into it
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 scripts/feature_prioritisation.py --framework ice -
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--json` to produce machine-readable output for downstream tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
### Feature Prioritisation — [Product/Team] — [Date]
|
### Feature Prioritisation — [Product/Team] — [Date]
|
||||||
|
|||||||
@@ -80,6 +80,29 @@ Recommend building: all Basic features first → Performance features for key us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Programmatic Helper
|
||||||
|
|
||||||
|
This skill ships with a stdlib-only Python script that computes ranking for the math-based frameworks (RICE, ICE) so feature scoring is consistent across sessions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.json --framework rice
|
||||||
|
|
||||||
|
# RICE from CSV
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
|
||||||
|
# ICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py features.json --framework ice
|
||||||
|
|
||||||
|
# Pipe into it
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 scripts/feature_prioritisation.py --framework ice -
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--json` to produce machine-readable output for downstream tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
### Feature Prioritisation — [Product/Team] — [Date]
|
### Feature Prioritisation — [Product/Team] — [Date]
|
||||||
|
|||||||
@@ -80,6 +80,29 @@ Recommend building: all Basic features first → Performance features for key us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Programmatic Helper
|
||||||
|
|
||||||
|
This skill ships with a stdlib-only Python script that computes ranking for the math-based frameworks (RICE, ICE) so feature scoring is consistent across sessions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.json --framework rice
|
||||||
|
|
||||||
|
# RICE from CSV
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
|
||||||
|
# ICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py features.json --framework ice
|
||||||
|
|
||||||
|
# Pipe into it
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 scripts/feature_prioritisation.py --framework ice -
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--json` to produce machine-readable output for downstream tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
### Feature Prioritisation — [Product/Team] — [Date]
|
### Feature Prioritisation — [Product/Team] — [Date]
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Feature prioritisation helper for the feature-prioritisation skill.
|
||||||
|
|
||||||
|
Computes ranking for common scoring frameworks so the same formulas and ordering
|
||||||
|
are applied consistently. Supports RICE and ICE with JSON input.
|
||||||
|
|
||||||
|
Input formats:
|
||||||
|
- JSON list (default): each item includes `name` and framework-specific fields.
|
||||||
|
- CSV: header-driven input when using --format csv.
|
||||||
|
|
||||||
|
RICE fields:
|
||||||
|
name,reach,impact,confidence,effort
|
||||||
|
|
||||||
|
ICE fields:
|
||||||
|
name,impact,confidence,ease
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python3 feature_prioritisation.py --framework rice initiatives.json
|
||||||
|
python3 feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 feature_prioritisation.py --framework ice -
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Feature:
|
||||||
|
name: str
|
||||||
|
scores: dict[str, float]
|
||||||
|
|
||||||
|
def rice_score(self) -> float:
|
||||||
|
return (self.scores["reach"] * self.scores["impact"] * self.scores["confidence"]) / self.scores["effort"]
|
||||||
|
|
||||||
|
def ice_score(self) -> float:
|
||||||
|
return self.scores["impact"] + self.scores["confidence"] + self.scores["ease"]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_confidence(value: float, framework: str) -> float:
|
||||||
|
"""Normalize confidence depending on framework conventions."""
|
||||||
|
if framework == "rice":
|
||||||
|
return value / 100.0 if value > 1 else value
|
||||||
|
# ICE uses a 1-10 convention in this skill; accept 0-1 and 1-10, 80/100 as percent fallback.
|
||||||
|
if value > 1:
|
||||||
|
if value > 10:
|
||||||
|
return value / 10.0
|
||||||
|
return value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _to_feature(name: str, values: dict[str, object], framework: str) -> Feature:
|
||||||
|
try:
|
||||||
|
if framework == "rice":
|
||||||
|
reach = float(values["reach"])
|
||||||
|
effort = float(values["effort"])
|
||||||
|
if effort <= 0:
|
||||||
|
raise ValueError("effort must be greater than 0")
|
||||||
|
return Feature(
|
||||||
|
name=name,
|
||||||
|
scores={
|
||||||
|
"reach": reach,
|
||||||
|
"impact": float(values["impact"]),
|
||||||
|
"confidence": _normalise_confidence(float(values["confidence"]), "rice"),
|
||||||
|
"effort": effort,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ICE
|
||||||
|
return Feature(
|
||||||
|
name=name,
|
||||||
|
scores={
|
||||||
|
"impact": float(values["impact"]),
|
||||||
|
"confidence": _normalise_confidence(float(values["confidence"]), "ice"),
|
||||||
|
"ease": float(values["ease"]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(f"Missing required field {exc} in feature '{name}'.") from None
|
||||||
|
|
||||||
|
|
||||||
|
def load_rice_json(rows: list[dict[str, object]]) -> list[Feature]:
|
||||||
|
return [_to_feature(str(row["name"]).strip(), row, "rice") for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def load_ice_json(rows: list[dict[str, object]]) -> list[Feature]:
|
||||||
|
return [_to_feature(str(row["name"]).strip(), row, "ice") for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_csv(text: str, framework: str) -> list[dict[str, str]]:
|
||||||
|
rows = list(csv.DictReader(io.StringIO(text)))
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
expected = {"rice": {"name", "reach", "impact", "confidence", "effort"},
|
||||||
|
"ice": {"name", "impact", "confidence", "ease"}}
|
||||||
|
present = set(rows[0].keys())
|
||||||
|
missing = expected[framework] - present
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"CSV format missing required columns: {', '.join(sorted(missing))}")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def load(text: str, fmt: str, framework: str) -> list[Feature]:
|
||||||
|
if fmt == "csv":
|
||||||
|
rows = _load_csv(text, framework)
|
||||||
|
if framework == "rice":
|
||||||
|
return load_rice_json(rows)
|
||||||
|
return load_ice_json(rows)
|
||||||
|
|
||||||
|
rows = json.loads(text)
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
raise ValueError("Input must be a list of feature objects.")
|
||||||
|
if framework == "rice":
|
||||||
|
return load_rice_json(rows)
|
||||||
|
return load_ice_json(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def rank(features: list[Feature], framework: str) -> list[dict]:
|
||||||
|
scored = []
|
||||||
|
for feature in features:
|
||||||
|
score = feature.rice_score() if framework == "rice" else feature.ice_score()
|
||||||
|
row = {"name": feature.name, "score": round(float(score), 2)}
|
||||||
|
row.update({k: v for k, v in feature.scores.items() if k != "score"})
|
||||||
|
scored.append(row)
|
||||||
|
|
||||||
|
scored.sort(key=lambda d: d["score"], reverse=True)
|
||||||
|
for index, row in enumerate(scored, start=1):
|
||||||
|
row["rank"] = index
|
||||||
|
return scored
|
||||||
|
|
||||||
|
|
||||||
|
def _render(ranked: list[dict], framework: str) -> str:
|
||||||
|
if framework == "rice":
|
||||||
|
header = f"{'#':>2} {'Feature':<30} {'Reach':>10} {'Impact':>7} {'Conf':>7} {'Effort':>7} {'RICE':>8}"
|
||||||
|
lines = ["Feature Prioritisation (RICE)", "=" * len(header), header, "-" * len(header)]
|
||||||
|
for row in ranked:
|
||||||
|
lines.append(
|
||||||
|
f"{row['rank']:>2} {row['name'][:30]:<30} "
|
||||||
|
f"{row['reach']:>10g} {row['impact']:>7g} {row['confidence']:>6.2f} {row['effort']:>7g} {row['score']:>8g}"
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
header = f"{'#':>2} {'Feature':<30} {'Impact':>7} {'Conf':>7} {'Ease':>7} {'ICE':>8}"
|
||||||
|
lines = ["Feature Prioritisation (ICE)", "=" * len(header), header, "-" * len(header)]
|
||||||
|
for row in ranked:
|
||||||
|
lines.append(
|
||||||
|
f"{row['rank']:>2} {row['name'][:30]:<30} "
|
||||||
|
f"{row['impact']:>7g} {row['confidence']:>6.2f} {row['ease']:>7g} {row['score']:>8g}"
|
||||||
|
)
|
||||||
|
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 input JSON/CSV file, or '-' for stdin.")
|
||||||
|
parser.add_argument("--framework", choices=["rice", "ice"], default="rice",
|
||||||
|
help="Scoring framework to use.")
|
||||||
|
parser.add_argument("--format", choices=["json", "csv"], help="Input format (inferred from extension when omitted).")
|
||||||
|
parser.add_argument("--json", action="store_true", dest="as_json", help="Emit ranked JSON instead of a table.")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.input == "-":
|
||||||
|
text = sys.stdin.read()
|
||||||
|
fmt = args.format or "json"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(args.input, "r", encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if args.format:
|
||||||
|
fmt = args.format
|
||||||
|
else:
|
||||||
|
fmt = "csv" if args.input.lower().endswith(".csv") else "json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
ranked = rank(load(text, fmt, args.framework), args.framework)
|
||||||
|
except (ValueError, json.JSONDecodeError, KeyError) as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(json.dumps(ranked, indent=2) if args.as_json else _render(ranked, args.framework))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -80,6 +80,29 @@ Recommend building: all Basic features first → Performance features for key us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Programmatic Helper
|
||||||
|
|
||||||
|
This skill ships with a stdlib-only Python script that computes ranking for the math-based frameworks (RICE, ICE) so feature scoring is consistent across sessions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.json --framework rice
|
||||||
|
|
||||||
|
# RICE from CSV
|
||||||
|
python3 scripts/feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
|
||||||
|
# ICE from JSON
|
||||||
|
python3 scripts/feature_prioritisation.py features.json --framework ice
|
||||||
|
|
||||||
|
# Pipe into it
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 scripts/feature_prioritisation.py --framework ice -
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--json` to produce machine-readable output for downstream tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
### Feature Prioritisation — [Product/Team] — [Date]
|
### Feature Prioritisation — [Product/Team] — [Date]
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Feature prioritisation helper for the feature-prioritisation skill.
|
||||||
|
|
||||||
|
Computes ranking for common scoring frameworks so the same formulas and ordering
|
||||||
|
are applied consistently. Supports RICE and ICE with JSON input.
|
||||||
|
|
||||||
|
Input formats:
|
||||||
|
- JSON list (default): each item includes `name` and framework-specific fields.
|
||||||
|
- CSV: header-driven input when using --format csv.
|
||||||
|
|
||||||
|
RICE fields:
|
||||||
|
name,reach,impact,confidence,effort
|
||||||
|
|
||||||
|
ICE fields:
|
||||||
|
name,impact,confidence,ease
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python3 feature_prioritisation.py --framework rice initiatives.json
|
||||||
|
python3 feature_prioritisation.py initiatives.csv --framework rice --format csv
|
||||||
|
printf '%s\n' '[{"name":"API refactor","impact":8,"confidence":80,"ease":5}]' \
|
||||||
|
| python3 feature_prioritisation.py --framework ice -
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Feature:
|
||||||
|
name: str
|
||||||
|
scores: dict[str, float]
|
||||||
|
|
||||||
|
def rice_score(self) -> float:
|
||||||
|
return (self.scores["reach"] * self.scores["impact"] * self.scores["confidence"]) / self.scores["effort"]
|
||||||
|
|
||||||
|
def ice_score(self) -> float:
|
||||||
|
return self.scores["impact"] + self.scores["confidence"] + self.scores["ease"]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_confidence(value: float, framework: str) -> float:
|
||||||
|
"""Normalize confidence depending on framework conventions."""
|
||||||
|
if framework == "rice":
|
||||||
|
return value / 100.0 if value > 1 else value
|
||||||
|
# ICE uses a 1-10 convention in this skill; accept 0-1 and 1-10, 80/100 as percent fallback.
|
||||||
|
if value > 1:
|
||||||
|
if value > 10:
|
||||||
|
return value / 10.0
|
||||||
|
return value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _to_feature(name: str, values: dict[str, object], framework: str) -> Feature:
|
||||||
|
try:
|
||||||
|
if framework == "rice":
|
||||||
|
reach = float(values["reach"])
|
||||||
|
effort = float(values["effort"])
|
||||||
|
if effort <= 0:
|
||||||
|
raise ValueError("effort must be greater than 0")
|
||||||
|
return Feature(
|
||||||
|
name=name,
|
||||||
|
scores={
|
||||||
|
"reach": reach,
|
||||||
|
"impact": float(values["impact"]),
|
||||||
|
"confidence": _normalise_confidence(float(values["confidence"]), "rice"),
|
||||||
|
"effort": effort,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ICE
|
||||||
|
return Feature(
|
||||||
|
name=name,
|
||||||
|
scores={
|
||||||
|
"impact": float(values["impact"]),
|
||||||
|
"confidence": _normalise_confidence(float(values["confidence"]), "ice"),
|
||||||
|
"ease": float(values["ease"]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(f"Missing required field {exc} in feature '{name}'.") from None
|
||||||
|
|
||||||
|
|
||||||
|
def load_rice_json(rows: list[dict[str, object]]) -> list[Feature]:
|
||||||
|
return [_to_feature(str(row["name"]).strip(), row, "rice") for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def load_ice_json(rows: list[dict[str, object]]) -> list[Feature]:
|
||||||
|
return [_to_feature(str(row["name"]).strip(), row, "ice") for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_csv(text: str, framework: str) -> list[dict[str, str]]:
|
||||||
|
rows = list(csv.DictReader(io.StringIO(text)))
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
expected = {"rice": {"name", "reach", "impact", "confidence", "effort"},
|
||||||
|
"ice": {"name", "impact", "confidence", "ease"}}
|
||||||
|
present = set(rows[0].keys())
|
||||||
|
missing = expected[framework] - present
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"CSV format missing required columns: {', '.join(sorted(missing))}")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def load(text: str, fmt: str, framework: str) -> list[Feature]:
|
||||||
|
if fmt == "csv":
|
||||||
|
rows = _load_csv(text, framework)
|
||||||
|
if framework == "rice":
|
||||||
|
return load_rice_json(rows)
|
||||||
|
return load_ice_json(rows)
|
||||||
|
|
||||||
|
rows = json.loads(text)
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
raise ValueError("Input must be a list of feature objects.")
|
||||||
|
if framework == "rice":
|
||||||
|
return load_rice_json(rows)
|
||||||
|
return load_ice_json(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def rank(features: list[Feature], framework: str) -> list[dict]:
|
||||||
|
scored = []
|
||||||
|
for feature in features:
|
||||||
|
score = feature.rice_score() if framework == "rice" else feature.ice_score()
|
||||||
|
row = {"name": feature.name, "score": round(float(score), 2)}
|
||||||
|
row.update({k: v for k, v in feature.scores.items() if k != "score"})
|
||||||
|
scored.append(row)
|
||||||
|
|
||||||
|
scored.sort(key=lambda d: d["score"], reverse=True)
|
||||||
|
for index, row in enumerate(scored, start=1):
|
||||||
|
row["rank"] = index
|
||||||
|
return scored
|
||||||
|
|
||||||
|
|
||||||
|
def _render(ranked: list[dict], framework: str) -> str:
|
||||||
|
if framework == "rice":
|
||||||
|
header = f"{'#':>2} {'Feature':<30} {'Reach':>10} {'Impact':>7} {'Conf':>7} {'Effort':>7} {'RICE':>8}"
|
||||||
|
lines = ["Feature Prioritisation (RICE)", "=" * len(header), header, "-" * len(header)]
|
||||||
|
for row in ranked:
|
||||||
|
lines.append(
|
||||||
|
f"{row['rank']:>2} {row['name'][:30]:<30} "
|
||||||
|
f"{row['reach']:>10g} {row['impact']:>7g} {row['confidence']:>6.2f} {row['effort']:>7g} {row['score']:>8g}"
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
header = f"{'#':>2} {'Feature':<30} {'Impact':>7} {'Conf':>7} {'Ease':>7} {'ICE':>8}"
|
||||||
|
lines = ["Feature Prioritisation (ICE)", "=" * len(header), header, "-" * len(header)]
|
||||||
|
for row in ranked:
|
||||||
|
lines.append(
|
||||||
|
f"{row['rank']:>2} {row['name'][:30]:<30} "
|
||||||
|
f"{row['impact']:>7g} {row['confidence']:>6.2f} {row['ease']:>7g} {row['score']:>8g}"
|
||||||
|
)
|
||||||
|
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 input JSON/CSV file, or '-' for stdin.")
|
||||||
|
parser.add_argument("--framework", choices=["rice", "ice"], default="rice",
|
||||||
|
help="Scoring framework to use.")
|
||||||
|
parser.add_argument("--format", choices=["json", "csv"], help="Input format (inferred from extension when omitted).")
|
||||||
|
parser.add_argument("--json", action="store_true", dest="as_json", help="Emit ranked JSON instead of a table.")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.input == "-":
|
||||||
|
text = sys.stdin.read()
|
||||||
|
fmt = args.format or "json"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(args.input, "r", encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if args.format:
|
||||||
|
fmt = args.format
|
||||||
|
else:
|
||||||
|
fmt = "csv" if args.input.lower().endswith(".csv") else "json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
ranked = rank(load(text, fmt, args.framework), args.framework)
|
||||||
|
except (ValueError, json.JSONDecodeError, KeyError) as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(json.dumps(ranked, indent=2) if args.as_json else _render(ranked, args.framework))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user