mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-13 04:26:28 +03:00
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>
186 lines
5.6 KiB
Python
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())
|