feat(check): security value, commit tables, MEDUSA install and kvb-think-choice
kvb-check-security gains security.py: it scans the changed files with MEDUSA, weights findings (critical -50, high -25, medium -10, low -2) and prints locations and counts only; without MEDUSA it asks for a review by hand passed as '~<n>'. kvb-check-score takes --security and --critical: the score multiplies by security/100, a critical finding blocks the commit (exit 4), security below 100 asks the operator like efficiency or speed over 100, and a 90/5 failure now lists every cause. --table and --commit-table print the values as markdown tables, shown before and after every commit. make install installs MEDUSA with pipx unless it is on the PATH. The new kvb-think-choice skill offers open decisions as multiple choice and checkboxes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0343a2f3ce
commit
62a0bcf7e9
12 files changed
+786
-123
No files matched your search
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fold fix confidence, break risk, efficiency and speed into one score line and one gate line.
|
||||
"""Fold fix confidence, break risk, efficiency, speed and security into one score and one gate verdict.
|
||||
|
||||
Args:
|
||||
--fix: fix confidence in percent, 0 to 100.
|
||||
@@ -7,29 +7,48 @@ Args:
|
||||
--efficiency: efficiency in percent above 0; `-` when not measured or not
|
||||
relevant, `new` when the change has no base. Both count as 100 in the score.
|
||||
--speed: speed in percent above 0, `-` or `new` as for --efficiency.
|
||||
--security: security in percent, 0 to 100, from security.py of kvb-check-security;
|
||||
`~<n>` for a value from a review by hand.
|
||||
--critical: count of critical security findings.
|
||||
--table: print one markdown table instead of the two lines.
|
||||
--commit-table <repo>: the same table, led by the worktree, branch and staged
|
||||
diff stat of that repository.
|
||||
--commit <sha>: with --commit-table, the diff stat of that commit instead of
|
||||
the staged one, plus a commit column.
|
||||
|
||||
Output:
|
||||
Line 1: `🎯 fix <f> % · 💥 break <b> % · 🌱 efficiency <e> % · ⚡ speed <s> % · 🏆 score <n>`,
|
||||
`–` in place of `<x> %` for `-`, `new` for `new`. Score is
|
||||
fix * (100 - break) / 100 * 100 / efficiency * 100 / speed, capped to 0..100, rounded half up.
|
||||
Line 1: `🎯 fix <f> % · 💥 break <b> % · 🌱 efficiency <e> % · ⚡ speed <s> % ·
|
||||
🛡️ security <n> % · 🏆 score <n>`, `–` in place of `<x> %` for `-`, `new` for `new`.
|
||||
Score is fix * (100 - break) / 100 * 100 / efficiency * 100 / speed * security / 100,
|
||||
capped to 0..100, rounded half up.
|
||||
Line 2: the gate verdict, `✅ gate: ...`, `⛔ gate: ...` or `⚠️ gate: ...`.
|
||||
|
||||
Exit codes:
|
||||
0 gate passed, 1 fix under 90 or break over 5, 2 usage error,
|
||||
3 efficiency or speed over 100 or new: ask the operator before the commit.
|
||||
3 efficiency or speed over 100 or new, or security under 100: ask the operator
|
||||
before the commit, 4 a critical security finding: do not commit.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
FIX_MIN = 90
|
||||
BREAK_MAX = 5
|
||||
COST_MAX = 100
|
||||
SAFE = 100
|
||||
DASH = "-"
|
||||
NEW = "new"
|
||||
ESTIMATE = "~"
|
||||
SCORE_HEADER = ("🎯 Fix", "💥 Break", "🌱 Efficiency", "⚡ Speed", "🛡️ Security", "🏆 Score", "Gate")
|
||||
COMMIT_HEADER = ("📁 Worktree", "🌿 Branch", "📊 Diff stat")
|
||||
SHORTSTAT = re.compile(
|
||||
r"(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?"
|
||||
)
|
||||
|
||||
|
||||
def percent(text: str) -> float:
|
||||
@@ -54,6 +73,17 @@ def cost(text: str) -> float | str:
|
||||
return value
|
||||
|
||||
|
||||
def security(text: str) -> tuple[float, str]:
|
||||
mark = ESTIMATE if text.startswith(ESTIMATE) else ""
|
||||
return percent(text.removeprefix(ESTIMATE)), mark
|
||||
|
||||
|
||||
def count(text: str) -> int:
|
||||
if not text.isdigit():
|
||||
raise argparse.ArgumentTypeError(f"{text!r} is not a count")
|
||||
return int(text)
|
||||
|
||||
|
||||
def number(value: float) -> str:
|
||||
return f"{value:g}"
|
||||
|
||||
@@ -70,8 +100,11 @@ def factor(value: float | str) -> float:
|
||||
return 100 / value if isinstance(value, float) else 1.0
|
||||
|
||||
|
||||
def score(fix: float, risk: float, efficiency: float | str, speed: float | str) -> int:
|
||||
raw = fix * (100 - risk) / 100 * factor(efficiency) * factor(speed)
|
||||
def score(args: argparse.Namespace) -> int:
|
||||
raw = (
|
||||
args.fix * (100 - args.risk) / 100 * factor(args.efficiency) * factor(args.speed)
|
||||
* args.security[0] / 100
|
||||
)
|
||||
return math.floor(min(max(raw, 0.0), 100.0) + 0.5)
|
||||
|
||||
|
||||
@@ -83,18 +116,67 @@ def over(name: str, value: float | str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def gate(fix: float, risk: float, efficiency: float | str, speed: float | str) -> tuple[str, int]:
|
||||
def gate(args: argparse.Namespace) -> tuple[str, str, int]:
|
||||
if args.critical > 0:
|
||||
return "⛔", "critical security finding: do not commit", 4
|
||||
failed = []
|
||||
if fix < FIX_MIN:
|
||||
failed.append(f"fix {number(fix)} % < {FIX_MIN}")
|
||||
if risk > BREAK_MAX:
|
||||
failed.append(f"break {number(risk)} % > {BREAK_MAX}")
|
||||
if args.fix < FIX_MIN:
|
||||
failed.append(f"fix {number(args.fix)} % < {FIX_MIN}")
|
||||
if args.risk > BREAK_MAX:
|
||||
failed.append(f"break {number(args.risk)} % > {BREAK_MAX}")
|
||||
value, mark = args.security
|
||||
unsafe = f"security {mark}{number(value)} % < {SAFE}" if value < SAFE else None
|
||||
asks = [text for text in (over("efficiency", args.efficiency), over("speed", args.speed), unsafe) if text]
|
||||
ask = f"{', '.join(asks)}: ask the operator before the commit" if asks else ""
|
||||
if failed:
|
||||
return f"⛔ gate: {', '.join(failed)}", 1
|
||||
costly = [text for text in (over("efficiency", efficiency), over("speed", speed)) if text]
|
||||
if costly:
|
||||
return f"⚠️ gate: {', '.join(costly)}: ask the operator before the commit", 3
|
||||
return f"✅ gate: {FIX_MIN}/{BREAK_MAX}, efficiency and speed <= {COST_MAX}", 0
|
||||
return "⛔", "; ".join(part for part in (", ".join(failed), ask) if part), 1
|
||||
if asks:
|
||||
return "⚠️", ask, 3
|
||||
return "✅", f"{FIX_MIN}/{BREAK_MAX}, efficiency and speed <= {COST_MAX}, security {SAFE} %", 0
|
||||
|
||||
|
||||
def git(repo: str, *args: str) -> str:
|
||||
try:
|
||||
result = subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True, check=False)
|
||||
except OSError as error:
|
||||
print(f"score: git {args[0]} in {repo}: {error}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if result.returncode != 0:
|
||||
print(f"score: git {args[0]} in {repo}: {result.stderr.strip()}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def diffstat(shortstat: str) -> str:
|
||||
match = SHORTSTAT.search(shortstat)
|
||||
files, added, removed = (int(g or 0) for g in match.groups()) if match else (0, 0, 0)
|
||||
return f"{files} file{'' if files == 1 else 's'} +{added}/-{removed}"
|
||||
|
||||
|
||||
def commit_cells(repo: str, commit: str | None) -> tuple[list[str], list[str]]:
|
||||
branch = git(repo, "branch", "--show-current") or f"detached at {git(repo, 'rev-parse', '--short', 'HEAD')}"
|
||||
if commit is None:
|
||||
stat = git(repo, "diff", "--cached", "--shortstat")
|
||||
return list(COMMIT_HEADER), [git(repo, "rev-parse", "--show-toplevel"), branch, diffstat(stat)]
|
||||
stat = git(repo, "show", "--shortstat", "--format=", commit)
|
||||
return (
|
||||
[*COMMIT_HEADER, "🔖 Commit"],
|
||||
[git(repo, "rev-parse", "--show-toplevel"), branch, diffstat(stat), git(repo, "rev-parse", "--short", commit)],
|
||||
)
|
||||
|
||||
|
||||
def cell(text: str) -> str:
|
||||
return text.replace("|", "\\|")
|
||||
|
||||
|
||||
def table(header: list[str], row: list[str]) -> str:
|
||||
return "\n".join(
|
||||
(
|
||||
"| " + " | ".join(header) + " |",
|
||||
"|" + " --- |" * len(header),
|
||||
"| " + " | ".join(cell(value) for value in row) + " |",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -103,14 +185,34 @@ def main() -> int:
|
||||
parser.add_argument("--break", dest="risk", type=percent, required=True)
|
||||
parser.add_argument("--efficiency", type=cost, required=True)
|
||||
parser.add_argument("--speed", type=cost, required=True)
|
||||
parser.add_argument("--security", type=security, required=True)
|
||||
parser.add_argument("--critical", type=count, required=True)
|
||||
layout = parser.add_mutually_exclusive_group()
|
||||
layout.add_argument("--table", action="store_true")
|
||||
layout.add_argument("--commit-table", metavar="REPO")
|
||||
parser.add_argument("--commit", metavar="SHA")
|
||||
args = parser.parse_args()
|
||||
total = score(args.fix, args.risk, args.efficiency, args.speed)
|
||||
print(
|
||||
f"🎯 fix {number(args.fix)} % · 💥 break {number(args.risk)} % · "
|
||||
f"🌱 efficiency {shown(args.efficiency)} · ⚡ speed {shown(args.speed)} · 🏆 score {total}"
|
||||
)
|
||||
verdict, code = gate(args.fix, args.risk, args.efficiency, args.speed)
|
||||
print(verdict)
|
||||
if args.commit and not args.commit_table:
|
||||
parser.error("--commit needs --commit-table")
|
||||
|
||||
total = score(args)
|
||||
emoji, verdict, code = gate(args)
|
||||
value, mark = args.security
|
||||
values = [
|
||||
f"{number(args.fix)} %", f"{number(args.risk)} %", shown(args.efficiency), shown(args.speed),
|
||||
f"{mark}{number(value)} %", str(total), f"{emoji} {verdict}",
|
||||
]
|
||||
if args.commit_table:
|
||||
header, cells = commit_cells(args.commit_table, args.commit)
|
||||
print(table([*header, *SCORE_HEADER], [*cells, *values]))
|
||||
elif args.table:
|
||||
print(table(list(SCORE_HEADER), values))
|
||||
else:
|
||||
print(
|
||||
f"🎯 fix {values[0]} · 💥 break {values[1]} · 🌱 efficiency {values[2]} · "
|
||||
f"⚡ speed {values[3]} · 🛡️ security {values[4]} · 🏆 score {total}"
|
||||
)
|
||||
print(f"{emoji} gate: {verdict}")
|
||||
return code
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user