Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .quale/ci-history.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,21 @@
{"timestamp": 1779985490.708004, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 24, "mirror_gap_ratio": 0.156, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 1}
{"timestamp": 1779985500.4287434, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 24, "mirror_gap_ratio": 0.156, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 1}
{"timestamp": 1779985501.299929, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 24, "mirror_gap_ratio": 0.156, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 1}
{"timestamp": 1779985964.562954, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779985971.1160638, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779991982.5523138, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779991997.8795328, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779991999.5090065, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779992721.2087755, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779992732.0732007, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779992733.1683042, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779993098.6285026, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779993104.057374, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779993492.9940534, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779993496.3423092, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779993534.5206554, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779993543.5418565, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1779993544.5609477, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 5, "blast_radius_count": 27, "mirror_gap_ratio": 0.158, "stable_touched_count": 5, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 3}], "clone_flagged": [{"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}], "new_identifier_count": 2}
{"timestamp": 1780006290.737936, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 24, "mirror_gap_ratio": 0.099, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/reports/__init__.py", "hub_rank": 1}, {"file": "quale/scanner.py", "hub_rank": 2}], "clone_flagged": [], "new_identifier_count": 0}
{"timestamp": 1780006301.5427566, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 24, "mirror_gap_ratio": 0.099, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/reports/__init__.py", "hub_rank": 1}, {"file": "quale/scanner.py", "hub_rank": 2}], "clone_flagged": [], "new_identifier_count": 0}
{"timestamp": 1780006302.606474, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 24, "mirror_gap_ratio": 0.099, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/reports/__init__.py", "hub_rank": 1}, {"file": "quale/scanner.py", "hub_rank": 2}], "clone_flagged": [], "new_identifier_count": 0}
32 changes: 32 additions & 0 deletions quale/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import math
from collections import defaultdict
from dataclasses import dataclass, field

Expand All @@ -23,6 +24,37 @@ def add_file(self, phrases: set[str]):
if a < b:
self.pairs[(a, b)] += 1

def pmi(self, a: str, b: str) -> float:
"""Pointwise Mutual Information: log2(P(a,b) / P(a)P(b))."""
if a == b:
return 0.0
pair_count = self.pairs.get((a, b) if a < b else (b, a), 0)
if pair_count == 0:
return 0.0
count_a = self.phrase_count.get(a, 0)
count_b = self.phrase_count.get(b, 0)
total = self.total_docs
if count_a == 0 or count_b == 0 or total == 0:
return 0.0
p_ab = pair_count / total
p_a = count_a / total
p_b = count_b / total
if p_a * p_b == 0:
return 0.0
return math.log2(p_ab / (p_a * p_b))

def top_pmi_for(self, phrase: str, limit: int = 10, min_freq: int = 1) -> list[tuple[str, float]]:
"""Return PMI-sorted partners for a phrase — what co-occurs most surprisingly?"""
partners: dict[str, int] = defaultdict(int)
for (a, b), count in self.pairs.items():
if a == phrase:
partners[b] = count
elif b == phrase:
partners[a] = count
scored = [(p, self.pmi(phrase, p)) for p in partners if self.phrase_count.get(p, 0) >= min_freq]
scored.sort(key=lambda x: -x[1])
return scored[:limit]

def cluster(self, min_cooccurrence: int = 3, min_phrases: int = 2) -> list[list[str]]:
"""Extract co-occurrence clusters — groups of phrases that frequently appear together."""
clusters: list[set[str]] = []
Expand Down
67 changes: 62 additions & 5 deletions quale/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5262,12 +5262,19 @@ def ci_trend_wrapper(
@core_app.command(name="risk", rich_help_panel="Code Analysis")
def risk_cmd(
path: Annotated[str, typer.Option("--path", "-p", help="Path to repo")] = ".",
mode: Annotated[str, typer.Option("--mode", "-m", help="Mode: full, hub, capillary")] = "full",
mode: Annotated[str, typer.Option("--mode", "-m", help="Mode: full, hub, capillary, co-change, anomaly")] = "full",
format: Annotated[str, typer.Option("--format", "-f", help="Output: human, json")] = "human",
ci: Annotated[bool, typer.Option("--ci", help="CI gate mode")] = False,
top_n: Annotated[int, typer.Option("--top-n", help="Results limit")] = 10,
) -> None:
"""Surface risky files — hub, capillary, and their intersection."""
from quale.reports import capillary_report, thanatosis_report, vulnerability_report
"""Surface risky files — hub, capillary, co-change prediction, or anomaly detection."""
from quale.reports import (
anomaly_report,
capillary_report,
co_change_report,
thanatosis_report,
vulnerability_report,
)
p = os.path.abspath(path)
if not vgit.is_repo(p):
typer.echo("Not a git repository.", err=True)
Expand All @@ -5280,6 +5287,12 @@ def risk_cmd(
data = capillary_report(path=p)
dt, cr = [], []
ch = [c["file"] for c in data.get("capillaries", [])]
elif mode == "co-change":
data = co_change_report(path=p, top_n=top_n)
dt, ch, cr = [], [], data.get("files", [])
elif mode == "anomaly":
data = anomaly_report(path=p, top_n=top_n)
dt, ch, cr = [], [], data.get("anomalies", [])
else:
data = vulnerability_report(path=p)
dt = data.get("don_touch", [])
Expand All @@ -5290,13 +5303,55 @@ def risk_cmd(
raise typer.Exit(1)
result = {"critical_files": cr, "hub_files": dt, "capillary_files": ch}
if format == "json":
if mode in ("co-change", "anomaly"):
typer.echo(json.dumps(data, indent=2))
return
typer.echo(json.dumps(result, indent=2))
if ci and cr:
raise typer.Exit(1)
if ci and not cr:
typer.echo(f"{ICON_CHECK} No critical files — CI gate passed")
if format == "json":
return
if mode == "co-change":
seen_files = set()
for entry in data.get("files", []):
target = entry.get("file", "")
first = True
for cc in entry.get("co_changes", []):
partner = cc.get("file", "")
if partner in seen_files:
continue
seen_files.add(partner)
if first:
typer.echo(f"{ICON_PRIMARY} Co-change prediction for {target}:")
first = False
pmi = cc.get("pmi", 0)
cop = cc.get("co_change_prob", 0)
fs = cc.get("fused_score", 0)
typer.echo(f" {partner} (pmi={pmi}, co-change={cop}, fused={fs})")
if not seen_files:
typer.echo("No co-change predictions above threshold.")
return
if mode == "anomaly":
stats = data.get("statistics", {})
anomalies = data.get("anomalies", [])
if stats:
typer.echo(f"{ICON_PRIMARY} PMI anomalies (mean={stats.get('mean_pmi')}, max={stats.get('max_pmi')}, top {len(anomalies)})")
for a in anomalies:
a_str = a.get("a", "")
b_str = a.get("b", "")
pmi_val = a.get("pmi", 0)
ta = a.get("p_a", 0)
tb = a.get("p_b", 0)
typer.echo(f" {ICON_WARN} {a_str} ↔ {b_str} PMI={pmi_val} P(a)={ta}, P(b)={tb}")
if not anomalies:
typer.echo("No structural anomalies found.")
return
if ci and cr:
raise typer.Exit(1)
if ci and not cr:
typer.echo(f"{ICON_CHECK} No critical files — CI gate passed")
if cr:
typer.echo(f"{ICON_WARN} Critical (Hub + Capillary):")
for f in cr:
Expand All @@ -5322,11 +5377,11 @@ def verify_cmd(
files: Annotated[list[str], typer.Option("--files", help="Changed file(s)")] = None,
diff: Annotated[str | None, typer.Option("--diff", help="Git ref")] = None,
task: Annotated[str | None, typer.Option("--task", "-t", help="Task description")] = None,
mode: Annotated[str, typer.Option("--mode", "-m", help="Mode: mc, scope, packet, full")] = "full",
mode: Annotated[str, typer.Option("--mode", "-m", help="Mode: mc, scope, packet, incomplete, full")] = "full",
format: Annotated[str, typer.Option("--format", "-f", help="Output: human, json")] = "human",
) -> None:
"""Verification pipeline — mc (pre-edit), packet (post-edit), scope (post-edit scope check)."""
from quale.reports import cartridge_report, guard_report, preflight_report, verify_scope
from quale.reports import cartridge_report, guard_report, incomplete_change_report, preflight_report, verify_scope
p = os.path.abspath(path)
if not vgit.is_repo(p):
typer.echo("Not a git repository.", err=True)
Expand All @@ -5339,6 +5394,8 @@ def verify_cmd(
data = verify_scope(path=p, contract_files=files or None, diff_ref=diff)
elif mode == "packet":
data = cartridge_report(path=p, files=files or None, diff_ref=diff, task=task)
elif mode == "incomplete":
data = incomplete_change_report(path=p, changed_files=files or None, diff_ref=diff)
else:
data = guard_report(path=p, file_path=files[0] if files else "", task=task or "")
if "error" in data:
Expand Down
Loading
Loading