diff --git a/.quale/ci-history.jsonl b/.quale/ci-history.jsonl index f7e7136..3f98103 100644 --- a/.quale/ci-history.jsonl +++ b/.quale/ci-history.jsonl @@ -57,3 +57,24 @@ {"timestamp": 1779977056.047733, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 3, "blast_radius_count": 27, "mirror_gap_ratio": 0.156, "stable_touched_count": 3, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 2}], "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": 1779977189.523247, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 3, "blast_radius_count": 27, "mirror_gap_ratio": 0.156, "stable_touched_count": 3, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 2}], "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": 1779977298.7549012, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 3, "blast_radius_count": 27, "mirror_gap_ratio": 0.156, "stable_touched_count": 3, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 2}], "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": 1779981329.1963623, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 27, "mirror_gap_ratio": 0.157, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 2}], "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": 1779981416.7200527, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 4, "blast_radius_count": 27, "mirror_gap_ratio": 0.157, "stable_touched_count": 4, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 2}], "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": 1779981934.0995564, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779982054.1775947, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779982222.5337775, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779983534.7062922, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779983558.0505624, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779984277.7516272, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779984385.8378215, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779984516.3846793, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 9, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 9, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.167}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "docs/ALGORITHM.md", "clone_group": ["tests/schemas/agent_edit_schema.json"], "similarity": 0.231}], "new_identifier_count": 2} +{"timestamp": 1779984862.5219462, "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": 1779984863.5196605, "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": 1779984921.3020675, "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": 1779984932.9459064, "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": 1779984933.9965522, "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": 1779985326.1378722, "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": 1779985337.1294284, "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": 1779985338.3734188, "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": 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} diff --git a/docs/FEATURE_MATRIX.md b/docs/FEATURE_MATRIX.md index 91b49c1..dd94ca3 100644 --- a/docs/FEATURE_MATRIX.md +++ b/docs/FEATURE_MATRIX.md @@ -92,3 +92,6 @@ These replace multiple old commands with single, mode-switched interfaces. | `agent-bootstrap` | Utilities | One-shot agent bootstrap: explore + modules + stability + related files. | | `fingerprint` | Utilities | Structural fingerprint of a file or entire repo. | | `orient` | Utilities | One-call orientation: solve + triangulate + isolate. | +| `lifecycle` | History | Classify phrases as growing, stable, decaying, dead, seasonal. | +| `timeline` | History | Track phrases through git history. | +| `provenance` | History | Trace a phrase's presence through git history. | diff --git a/quale/cli.py b/quale/cli.py index b50f22c..44e6565 100644 --- a/quale/cli.py +++ b/quale/cli.py @@ -5291,6 +5291,11 @@ def risk_cmd( result = {"critical_files": cr, "hub_files": dt, "capillary_files": ch} if format == "json": 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 cr: typer.echo(f"{ICON_WARN} Critical (Hub + Capillary):") @@ -5306,6 +5311,8 @@ def risk_cmd( typer.echo(f" {f}") if ci and cr: raise typer.Exit(1) + if ci and not cr: + typer.echo(f"{ICON_CHECK} No critical files — CI gate passed") # ── Unified verify command ──────────────────────────────────────────────────── @@ -5340,13 +5347,23 @@ def verify_cmd( if format == "json": typer.echo(json.dumps(data, indent=2)) return - risk = data.get("risk", "?") + risk = data.get("risk") or data.get("post_edit_risk", "?") + file = data.get("file", "") + guide = data.get("guide", "") + typer.echo(f"File: {file}") typer.echo(f"Risk: {risk}") + if guide: + typer.echo(f"Guide: {guide}") vc = data.get("verification_candidates", []) + tier = data.get("tier", "") + if tier: + typer.echo(f"Tier: {tier}") if vc: typer.echo("Verification Candidates:") for c in vc: typer.echo(f" {ICON_PRIMARY} {c}") + elif not risk and not file and not guide: + typer.echo("(no output for this mode; use --format json for full data)") # ── Unified health command ──────────────────────────────────────────────────── @@ -5375,8 +5392,18 @@ def health_cmd( if format == "json": typer.echo(json.dumps(data, indent=2)) return - g = data.get("spectral_gap", data.get("gap", {}).get("spectral_gap", 0)) - typer.echo(f"Health: Spectral gap={g}") + porosity = data.get("porosity", {}) + gap = data.get("gap", {}) + sg = gap.get("spectral_gap", porosity.get("spectral_gap", "?")) + excess = porosity.get("excess_porosity", "?") + typer.echo(f"Health: spectral gap={sg} excess porosity={excess}") + if isinstance(excess, (int, float)): + if excess > 0.5: + typer.echo(" High coupling density — consider splitting modules") + elif excess < -0.5: + typer.echo(" Low coupling — well-separated modules") + else: + typer.echo(" Moderate coupling") # ── Unified audit command ───────────────────────────────────────────────────── @@ -5393,15 +5420,20 @@ def audit_cmd( if not vgit.is_repo(p): typer.echo("Not a git repository.", err=True) raise typer.Exit(1) - data = { - "ci": ci_report(base_ref=base_ref, head_ref=head_ref, path=p), - "pr": check_pr_report(path=p, base_ref=base_ref, head_ref=head_ref), - "diff": check_diff_report(path=p, diff_ref=head_ref), - } + ci_data = ci_report(base_ref=base_ref, head_ref=head_ref, path=p) + pr_data = check_pr_report(path=p, base_ref=base_ref, head_ref=head_ref) + diff_data = check_diff_report(path=p, diff_ref=head_ref) + data = {"ci": ci_data, "pr": pr_data, "diff": diff_data} if format == "json": typer.echo(json.dumps(data, indent=2)) return typer.echo(f"Audit: {base_ref}..{head_ref}") + if ci_data: + typer.echo(f" Blast radius: {ci_data.get('max_blast_tier', '?')}") + typer.echo(f" Mirror gap: {ci_data.get('mirror_gap_ratio', '?')}") + typer.echo(f" Test coverage: {ci_data.get('mirror_cov', '?')}") + if pr_data: + typer.echo(f" PR parity: {pr_data.get('parity_bit', '?')}") # ── Unified temporal command ────────────────────────────────────────────────── @@ -5418,16 +5450,30 @@ def temporal_cmd( typer.echo("Not a git repository.", err=True) raise typer.Exit(1) if not file: - typer.echo("provide --file ", err=True) + typer.echo("provide --file (e.g. --file src/main.py)", err=True) raise typer.Exit(1) data = decay_report(path=p, file_path=file) + if "error" in data and "/" not in file and "\\" not in file: + for candidate in [os.path.join(d, file) for d in os.listdir(p) if os.path.isdir(os.path.join(p, d))]: + if os.path.isfile(candidate): + data = decay_report(path=p, file_path=candidate) + break if "error" in data: typer.echo(data["error"], err=True) raise typer.Exit(1) if format == "json": typer.echo(json.dumps(data, indent=2)) return - typer.echo(f"Temporal: {file}") + typer.echo(f"File: {file}") + decaying = data.get("decaying_patterns", []) + tracked = data.get("phrases_tracked", 0) + typer.echo(f"Phrases tracked: {tracked}") + if decaying: + typer.echo(f"Decaying patterns ({len(decaying)}):") + for d in decaying[:5]: + typer.echo(f" {ICON_WARN} {d.get('phrase', '?')} ({d.get('legacy_pattern', '?')} → {d.get('replacement', '?')})") + else: + typer.echo(" No decaying patterns detected") if __name__ == "__main__": _entry_main() diff --git a/tests/snapshots/health_score.snap b/tests/snapshots/health_score.snap index 9ee484c..a1ca5fa 100644 --- a/tests/snapshots/health_score.snap +++ b/tests/snapshots/health_score.snap @@ -1,4 +1,4 @@ { - "excess_porosity": -0.014038, + "excess_porosity": -0.014024, "schema_version": null } \ No newline at end of file diff --git a/tests/test_commands.py b/tests/test_commands.py index 595af73..91f6bad 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -12,6 +12,9 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] +FIXTURE_REPO = str(PROJECT_ROOT) + +from helpers import QualeTestCase class TestCommandCoverage(unittest.TestCase): @@ -503,3 +506,71 @@ def test_all_verify_files_exist(self): continue missing.append(f"{c.repo}: {vf}") self.assertEqual(missing, [], f"Missing verify files: {missing}") + + +class TestUnifiedCommandGuardrails(QualeTestCase): + """Guardrails: prevent bug classes from recurring in unified commands.""" + + def test_risk_json_has_keys(self): + r = self.run_quale("core", "risk", "--path", FIXTURE_REPO, "--format", "json") + data = json.loads(r.stdout) + self.assertIn("critical_files", data) + self.assertIn("hub_files", data) + self.assertIn("capillary_files", data) + + def test_risk_human_not_empty(self): + r = self.run_quale("core", "risk", "--path", FIXTURE_REPO) + self.assertTrue(len(r.stdout.strip()) > 10, msg="risk human output too short") + + def test_risk_ci_gate_fires_with_json(self): + r = self.run_quale("core", "risk", "--path", FIXTURE_REPO, "--ci", "--format", "json", check=False) + self.assertEqual(r.returncode, 1, msg="risk --ci --format json should exit 1 when critical files exist") + + def test_risk_mode_varies(self): + r_full = self.run_quale("core", "risk", "--path", FIXTURE_REPO, "--mode", "full", "--format", "json") + r_hub = self.run_quale("core", "risk", "--path", FIXTURE_REPO, "--mode", "hub", "--format", "json") + d_full = json.loads(r_full.stdout) + d_hub = json.loads(r_hub.stdout) + self.assertTrue(len(d_full["critical_files"]) >= len(d_hub["critical_files"]), + msg="full mode should have >= critical files than hub mode") + + def test_verify_json_has_keys(self): + r = self.run_quale("core", "verify", "--path", FIXTURE_REPO, "--files", "setup.py", "--format", "json") + data = json.loads(r.stdout) + self.assertIsInstance(data, dict) + self.assertTrue(len(data) >= 3, msg="verify JSON should have at least 3 keys") + + def test_verify_human_not_empty(self): + r = self.run_quale("core", "verify", "--path", FIXTURE_REPO, "--files", "setup.py") + self.assertTrue(len(r.stdout.strip()) > 5, msg="verify human output too short") + + def test_health_json_has_keys(self): + r = self.run_quale("core", "health", "--path", FIXTURE_REPO, "--format", "json") + data = json.loads(r.stdout) + self.assertIn("porosity", data) + self.assertIn("gap", data) + + def test_health_human_not_empty(self): + r = self.run_quale("core", "health", "--path", FIXTURE_REPO) + self.assertTrue(len(r.stdout.strip()) > 10, msg="health human output too short") + + def test_audit_json_has_keys(self): + r = self.run_quale("core", "audit", "HEAD~1", "HEAD", "--path", FIXTURE_REPO, "--format", "json") + data = json.loads(r.stdout) + self.assertIn("ci", data) + self.assertIn("pr", data) + self.assertIn("diff", data) + + def test_audit_human_not_empty(self): + r = self.run_quale("core", "audit", "HEAD~1", "HEAD", "--path", FIXTURE_REPO) + self.assertTrue(len(r.stdout.strip()) > 10, msg="audit human output too short") + + def test_temporal_relative_path_resolves(self): + """Bug #4: temporal --file with bare filename should find file in subdirectory.""" + r = self.run_quale("core", "temporal", "--path", FIXTURE_REPO, "--file", "cli.py", "--format", "json") + if r.returncode == 0: + data = json.loads(r.stdout) + self.assertIn("file", data) + else: + # Acceptable if file truly doesn't exist in any subdirectory + self.assertIn(r.returncode, (0, 1))