Files
magnus919_agent-skills/qa-methodology/scripts/risk-prioritize.py
T
usernameandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 974fde017c feat(qa-methodology): add 2 stdlib-only CLI scripts and colocated pytest suite
Add risk-prioritize.py and check-ac-testability.py under scripts/ with:
- #!/usr/bin/env python3 shebang, stdlib-only imports
- --json and --help flags, idempotent, non-interactive
- Graceful malformed-input handling (concise stderr, no traceback)
- Documented exit codes (0 success, 1/2 error)

Add tests/ with 46 unittest.TestCase tests covering P×I ranking math,
--json parseability, AC testability classification, exit codes, --help,
malformed input, and idempotency. Tests are discoverable by both pytest
and unittest discovery (check-artifacts.py compatible).

Add qa-methodology/pytest.ini to override root coverage config.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-01 18:28:00 -04:00

186 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Risk prioritization CLI for qa-methodology.
Reads a JSON array of risk items, computes score = probability x impact,
ranks descending (deterministic tie-break by id), and outputs a
human-readable table by default or machine-parseable JSON with --json.
Input format (JSON array):
[{"id": "auth-bypass", "probability": 4, "impact": 5}, ...]
Each item requires:
- id: non-empty string
- probability: integer 1-5
- impact: integer 1-5
Exit codes:
0 success
1 malformed input (bad JSON, missing/invalid fields, wrong types)
2 usage error (argparse)
"""
import argparse
import json
import sys
def parse_args(argv=None):
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
prog="risk-prioritize.py",
description=(
"Compute risk priority scores (probability x impact) and rank "
"risk items for test allocation."
),
epilog="Exit codes: 0 success, 1 malformed input, 2 usage error.",
)
parser.add_argument(
"input",
nargs="?",
default="-",
metavar="INPUT_JSON",
help=(
"Path to a JSON file containing an array of risk items "
"(default: stdin). Each item: {\"id\": str, \"probability\": 1-5, "
"\"impact\": 1-5}."
),
)
parser.add_argument(
"--json",
action="store_true",
dest="json_output",
help="Output results as machine-parseable JSON instead of a table.",
)
return parser.parse_args(argv)
def validate_item(item, index):
"""Validate a single risk item. Returns (item, error_message)."""
if not isinstance(item, dict):
return None, "item {} is not an object".format(index)
item_id = item.get("id")
if item_id is None:
return None, "item {} missing required field 'id'".format(index)
if not isinstance(item_id, str) or not item_id.strip():
return None, "item {} field 'id' must be a non-empty string".format(index)
prob = item.get("probability")
if prob is None:
return None, "item '{}' missing required field 'probability'".format(item_id)
impact = item.get("impact")
if impact is None:
return None, "item '{}' missing required field 'impact'".format(item_id)
# Accept int only (not bool, not float)
if isinstance(prob, bool) or not isinstance(prob, int):
return None, "item '{}' field 'probability' must be an integer".format(item_id)
if isinstance(impact, bool) or not isinstance(impact, int):
return None, "item '{}' field 'impact' must be an integer".format(item_id)
if not (1 <= prob <= 5):
return None, "item '{}' field 'probability' must be 1-5".format(item_id)
if not (1 <= impact <= 5):
return None, "item '{}' field 'impact' must be 1-5".format(item_id)
return item, None
def load_and_validate(source_text):
"""Parse JSON text and validate the risk items array.
Returns (items, None) on success or (None, error_message) on failure.
"""
try:
data = json.loads(source_text)
except (json.JSONDecodeError, ValueError) as exc:
return None, "invalid JSON: {}".format(exc)
if not isinstance(data, list):
return None, "input must be a JSON array of risk items"
if len(data) == 0:
return None, "input array must contain at least one risk item"
items = []
for i, raw in enumerate(data):
item, err = validate_item(raw, i)
if err:
return None, err
items.append(item)
return items, None
def compute_rankings(items):
"""Compute score and rank items descending by score, then by id ascending.
Returns a new list of dicts with 'score' and 'rank' added.
Does not mutate the input.
"""
scored = []
for item in items:
scored.append({
"id": item["id"],
"probability": item["probability"],
"impact": item["impact"],
"score": item["probability"] * item["impact"],
})
# Sort descending by score, then ascending by id for deterministic tie-break
scored.sort(key=lambda x: (-x["score"], x["id"]))
for rank, entry in enumerate(scored, start=1):
entry["rank"] = rank
return scored
def format_table(rankings):
"""Format rankings as a human-readable table."""
lines = []
header = "{:<4} {:<30} {:>5} {:>5} {:>5}".format(
"Rank", "ID", "Prob", "Imp", "Score"
)
lines.append(header)
lines.append("-" * len(header))
for entry in rankings:
lines.append(
"{:<4} {:<30} {:>5} {:>5} {:>5}".format(
entry["rank"],
entry["id"],
entry["probability"],
entry["impact"],
entry["score"],
)
)
return "\n".join(lines)
def main(argv=None):
"""Entry point."""
args = parse_args(argv)
# Read input
try:
if args.input == "-":
source_text = sys.stdin.read()
else:
with open(args.input, "r", encoding="utf-8") as fh:
source_text = fh.read()
except OSError as exc:
print("error: cannot read input: {}".format(exc), file=sys.stderr)
return 1
if not source_text.strip():
print("error: input is empty", file=sys.stderr)
return 1
items, err = load_and_validate(source_text)
if err:
print("error: {}".format(err), file=sys.stderr)
return 1
rankings = compute_rankings(items)
if args.json_output:
print(json.dumps(rankings, indent=2))
else:
print(format_table(rankings))
return 0
if __name__ == "__main__":
sys.exit(main())