codejury 0.14.1__tar.gz → 0.14.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {codejury-0.14.1 → codejury-0.14.3}/PKG-INFO +1 -1
  2. {codejury-0.14.1 → codejury-0.14.3}/codejury/cli.py +10 -83
  3. {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/debate.py +35 -10
  4. {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/engine.py +1 -1
  5. codejury-0.14.3/codejury/diff/runner.py +85 -0
  6. {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/PKG-INFO +1 -1
  7. {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/SOURCES.txt +4 -4
  8. {codejury-0.14.1 → codejury-0.14.3}/pyproject.toml +1 -1
  9. {codejury-0.14.1 → codejury-0.14.3}/tests/test_cli_audit.py +8 -7
  10. {codejury-0.14.1 → codejury-0.14.3}/tests/test_diff_debate.py +23 -0
  11. {codejury-0.14.1 → codejury-0.14.3}/tests/test_json_parse.py +1 -1
  12. codejury-0.14.1/tests/test_diff_report.py → codejury-0.14.3/tests/test_report.py +1 -1
  13. codejury-0.14.1/codejury/infrastructure/__init__.py +0 -4
  14. {codejury-0.14.1 → codejury-0.14.3}/LICENSE +0 -0
  15. {codejury-0.14.1 → codejury-0.14.3}/README.md +0 -0
  16. {codejury-0.14.1 → codejury-0.14.3}/codejury/__init__.py +0 -0
  17. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/agent/full-review.md +0 -0
  18. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/agent/security-review-memory.md +0 -0
  19. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/entrypoints.yaml +0 -0
  20. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/SKILL.md +0 -0
  21. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/business-logic.md +0 -0
  22. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/code-injection.md +0 -0
  23. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/command-injection.md +0 -0
  24. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/cross-site-request-forgery.md +0 -0
  25. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/cross-site-scripting.md +0 -0
  26. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/hardcoded-secrets.md +0 -0
  27. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/http-response-splitting.md +0 -0
  28. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/improper-authentication.md +0 -0
  29. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/information-exposure.md +0 -0
  30. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-cryptography.md +0 -0
  31. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-deserialization.md +0 -0
  32. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
  33. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-transport.md +0 -0
  34. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/jwt-validation.md +0 -0
  35. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/mass-assignment.md +0 -0
  36. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/missing-authorization.md +0 -0
  37. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/open-redirect.md +0 -0
  38. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/path-traversal.md +0 -0
  39. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/race-condition.md +0 -0
  40. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/replay-attack.md +0 -0
  41. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/server-side-request-forgery.md +0 -0
  42. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/server-side-template-injection.md +0 -0
  43. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/session-fixation.md +0 -0
  44. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/sql-injection.md +0 -0
  45. {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/xml-external-entity.md +0 -0
  46. {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/__init__.py +0 -0
  47. {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/debate_prompts.py +0 -0
  48. {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/findings_filter.py +0 -0
  49. {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/prompts.py +0 -0
  50. {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/rules.py +0 -0
  51. {codejury-0.14.1 → codejury-0.14.3}/codejury/domain/__init__.py +0 -0
  52. {codejury-0.14.1 → codejury-0.14.3}/codejury/domain/finding.py +0 -0
  53. {codejury-0.14.1/codejury/infrastructure → codejury-0.14.3/codejury}/json_parse.py +0 -0
  54. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/__init__.py +0 -0
  55. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/anthropic.py +0 -0
  56. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/base.py +0 -0
  57. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/factory.py +0 -0
  58. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/litellm.py +0 -0
  59. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/mock.py +0 -0
  60. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/openai.py +0 -0
  61. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/openai_format.py +0 -0
  62. {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/retry.py +0 -0
  63. {codejury-0.14.1 → codejury-0.14.3}/codejury/repo/__init__.py +0 -0
  64. {codejury-0.14.1 → codejury-0.14.3}/codejury/repo/model.py +0 -0
  65. {codejury-0.14.1 → codejury-0.14.3}/codejury/repo/scaffold.py +0 -0
  66. {codejury-0.14.1/codejury/diff → codejury-0.14.3/codejury}/report.py +0 -0
  67. {codejury-0.14.1 → codejury-0.14.3}/codejury/resources.py +0 -0
  68. {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/dependency_links.txt +0 -0
  69. {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/entry_points.txt +0 -0
  70. {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/requires.txt +0 -0
  71. {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/top_level.txt +0 -0
  72. {codejury-0.14.1 → codejury-0.14.3}/setup.cfg +0 -0
  73. {codejury-0.14.1 → codejury-0.14.3}/tests/test_anthropic_provider.py +0 -0
  74. {codejury-0.14.1 → codejury-0.14.3}/tests/test_diff_engine.py +0 -0
  75. {codejury-0.14.1 → codejury-0.14.3}/tests/test_litellm_provider.py +0 -0
  76. {codejury-0.14.1 → codejury-0.14.3}/tests/test_openai_format.py +0 -0
  77. {codejury-0.14.1 → codejury-0.14.3}/tests/test_openai_provider.py +0 -0
  78. {codejury-0.14.1 → codejury-0.14.3}/tests/test_repo_model.py +0 -0
  79. {codejury-0.14.1 → codejury-0.14.3}/tests/test_repo_scaffold.py +0 -0
  80. {codejury-0.14.1 → codejury-0.14.3}/tests/test_retry_provider.py +0 -0
  81. {codejury-0.14.1 → codejury-0.14.3}/tests/test_rules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.14.1
3
+ Version: 0.14.3
4
4
  Summary: AI code security review: an adversarial diff-audit engine and an agent-driven whole-repo review methodology, with security knowledge as rich rules
5
5
  Author: AISecLabs
6
6
  License-Expression: MIT
@@ -1,14 +1,15 @@
1
- """codejury CLI.
1
+ """codejury CLI: thin argument parsing and dispatch.
2
2
 
3
- Two entry points matched to their nature:
3
+ Two paths matched to their nature:
4
4
 
5
- - ``audit`` runs the coded diff engine over a unified diff: a single balanced
6
- call (standard) or the adversarial Finder/Challenger/Judge pass.
7
- - ``full-review`` scaffolds a workspace and prints the methodology for an
5
+ - ``review diff`` runs the coded diff engine over a unified diff: a single
6
+ balanced call (standard) or the adversarial Finder/Challenger/Judge pass.
7
+ - ``review repo`` scaffolds a workspace and prints the methodology for an
8
8
  interactive agent to run a whole-repo review (it does not run an LLM pipeline,
9
9
  which a single call cannot do for a whole codebase).
10
10
 
11
- ``dry-run`` exercises the diff engine with a mock provider and no API key.
11
+ ``review diff --dry-run`` exercises the engine with a mock provider and no key.
12
+ The audit orchestration itself lives in ``codejury.diff.runner``.
12
13
  """
13
14
 
14
15
  from __future__ import annotations
@@ -17,20 +18,9 @@ import argparse
17
18
  import subprocess
18
19
  import sys
19
20
 
20
- import dataclasses
21
-
22
21
  from codejury import __version__
23
- from codejury.diff.debate import AdversarialAuditRunner
24
- from codejury.diff.engine import AuditRunner
25
- from codejury.diff.findings_filter import FindingsFilter
26
- from codejury.diff.report import gate, render
27
- from codejury.diff.rules import allowed_categories, normalize_category
28
- from codejury.domain.finding import Finding
29
-
30
- # A diff larger than this is audited file-by-file so a big PR does not overflow
31
- # the model's context and silently truncate the reply.
32
- _MAX_DIFF_CHARS = 60_000
33
- from codejury.repo.scaffold import scaffold
22
+ from codejury.report import gate, render
23
+ from codejury.diff.runner import audit_diff
34
24
  from codejury.providers.factory import (
35
25
  DEFAULT_API_BASE,
36
26
  DEFAULT_API_KEY,
@@ -42,75 +32,12 @@ from codejury.providers.factory import (
42
32
  make_provider,
43
33
  )
44
34
  from codejury.providers.mock import MockProvider
35
+ from codejury.repo.scaffold import scaffold
45
36
 
46
37
  _FORMATS = ("text", "markdown", "json", "sarif")
47
38
  _FAIL_ON = ("critical", "high", "medium", "low")
48
39
 
49
40
 
50
- def _split_diff_by_file(diff: str) -> list[str]:
51
- """Split a unified diff into one diff per file (`diff --git ...` boundaries)."""
52
- chunks: list[str] = []
53
- cur: list[str] = []
54
- for line in diff.splitlines(keepends=True):
55
- if line.startswith("diff --git ") and cur:
56
- chunks.append("".join(cur))
57
- cur = []
58
- cur.append(line)
59
- if cur:
60
- chunks.append("".join(cur))
61
- return chunks or ([diff] if diff.strip() else [])
62
-
63
-
64
- def _dedup_findings(findings: list[Finding]) -> list[Finding]:
65
- seen: set = set()
66
- out: list[Finding] = []
67
- for f in findings:
68
- k = (f.file, f.line, f.category, f.description)
69
- if k not in seen:
70
- seen.add(k)
71
- out.append(f)
72
- return out
73
-
74
-
75
- def audit_diff(
76
- diff: str,
77
- *,
78
- provider,
79
- model: str,
80
- mode: str = "standard",
81
- max_rounds: int = 3,
82
- filter_findings: bool = True,
83
- finder_model: str | None = None,
84
- challenger_model: str | None = None,
85
- judge_model: str | None = None,
86
- exclude_paths: tuple[str, ...] = (),
87
- ) -> tuple[list[Finding], list[tuple[Finding, str]]]:
88
- """Audit a diff and return (kept findings, dropped (finding, reason)).
89
-
90
- A diff over the size budget is audited one file at a time so it does not
91
- overflow the context. Finding categories are normalized to the rule-id set.
92
- ``exclude_paths`` are operator-supplied path substrings to drop."""
93
- def _run_one(d: str) -> list[Finding]:
94
- if mode == "adversarial":
95
- return AdversarialAuditRunner(
96
- provider=provider, model=model,
97
- finder_model=finder_model, challenger_model=challenger_model, judge_model=judge_model,
98
- ).run(d, max_rounds=max_rounds).findings
99
- return AuditRunner(provider=provider, model=model).run(d)
100
-
101
- if len(diff) > _MAX_DIFF_CHARS:
102
- chunks = _split_diff_by_file(diff)
103
- findings = _dedup_findings([f for c in chunks for f in _run_one(c)])
104
- else:
105
- findings = _run_one(diff)
106
-
107
- allowed = set(allowed_categories())
108
- findings = [dataclasses.replace(f, category=normalize_category(f.category, allowed)) for f in findings]
109
- if filter_findings:
110
- return FindingsFilter(exclude_paths=exclude_paths).filter(findings)
111
- return findings, []
112
-
113
-
114
41
  def _read_diff(args) -> str:
115
42
  if args.diff_file:
116
43
  with open(args.diff_file, encoding="utf-8") as f:
@@ -21,7 +21,7 @@ from codejury.diff.debate_prompts import (
21
21
  )
22
22
  from codejury.diff.rules import rules_for_diff
23
23
  from codejury.domain.finding import Finding, findings_from_list
24
- from codejury.infrastructure.json_parse import extract_json_object
24
+ from codejury.json_parse import extract_json_object
25
25
  from codejury.providers.base import Message, Provider
26
26
 
27
27
 
@@ -56,6 +56,28 @@ def _dedup(items: list[dict]) -> list[dict]:
56
56
  return out
57
57
 
58
58
 
59
+ _DISMISS_VERDICTS = frozenset({"dismiss", "dismissed", "false_positive", "reject", "rejected"})
60
+
61
+
62
+ def _loc(d: dict) -> str:
63
+ line = d.get("line")
64
+ return f"{d.get('file')}:{line}" if line else str(d.get("file") or "")
65
+
66
+
67
+ def _apply_dismissals(findings: list[dict], rebuttals: list[dict]) -> list[dict]:
68
+ """Drop findings the challenger dismissed. The challenger is recall-safe (it
69
+ dismisses only when the diff shows a safe pattern: a parameterized query,
70
+ basename, an allowlist, shell=False), so honoring its dismissals is sound even
71
+ when the judge is unavailable."""
72
+ dismissed = {
73
+ str(r.get("target")) for r in rebuttals
74
+ if str(r.get("verdict", "")).strip().lower() in _DISMISS_VERDICTS
75
+ }
76
+ if not dismissed:
77
+ return findings
78
+ return [f for f in findings if _loc(f) not in dismissed and str(f.get("file") or "") not in dismissed]
79
+
80
+
59
81
  class AdversarialAuditRunner:
60
82
  def __init__(
61
83
  self,
@@ -116,16 +138,19 @@ class AdversarialAuditRunner:
116
138
  rebuttals = _dicts(challenger.get("rebuttals"))
117
139
  new_findings = _dicts(challenger.get("new_findings"))
118
140
 
119
- verdict, judge_ok = self._ask(
120
- JUDGE_SYSTEM,
121
- judge_prompt(diff, finder_findings, rebuttals, new_findings, context=context),
122
- self._judge_model,
123
- )
141
+ jp = judge_prompt(diff, finder_findings, rebuttals, new_findings, context=context)
142
+ verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
143
+ if not judge_ok:
144
+ # the judge is the filter that controls false positives, and an
145
+ # unusable reply is often transient (a flaky proxy, a blocked
146
+ # request); re-ask once before giving up on it
147
+ verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
124
148
  if not judge_ok:
125
- # the judge reply was unusable (provider error, blocked request,
126
- # prose): do not let that silently drop the candidate findings;
127
- # fall back to the unjudged finder + challenger set, flagged degraded
128
- fallback = _dedup(finder_findings + new_findings)
149
+ # judge still unusable: degrade, but apply the recall-safe
150
+ # challenger's dismissals so a transient judge outage does not pass
151
+ # through findings the challenger already showed are safe (this is
152
+ # what otherwise inflates false positives in degraded runs)
153
+ fallback = _dedup(_apply_dismissals(finder_findings, rebuttals) + new_findings)
129
154
  judged = AdversarialResult(findings=findings_from_list(fallback), rounds=rounds, degraded=True)
130
155
  degraded = True
131
156
  break
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from codejury.domain.finding import Finding, findings_from_list
11
11
  from codejury.diff.prompts import SYSTEM, standard_audit_prompt
12
12
  from codejury.diff.rules import rules_for_diff
13
- from codejury.infrastructure.json_parse import extract_json_object
13
+ from codejury.json_parse import extract_json_object
14
14
  from codejury.providers.base import Message, Provider
15
15
 
16
16
 
@@ -0,0 +1,85 @@
1
+ """Diff-audit orchestration: run a diff through the engine and clean the result.
2
+
3
+ The library entry point behind `review diff`. Picks the standard or adversarial
4
+ engine, audits a large diff one file at a time so a big PR does not overflow the
5
+ model context, normalizes finding categories onto the rule-id set, and applies
6
+ the false-positive filter. Kept out of the CLI so it can be called as a library.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+
13
+ from codejury.diff.debate import AdversarialAuditRunner
14
+ from codejury.diff.engine import AuditRunner
15
+ from codejury.diff.findings_filter import FindingsFilter
16
+ from codejury.diff.rules import allowed_categories, normalize_category
17
+ from codejury.domain.finding import Finding
18
+
19
+ # A diff larger than this is audited file-by-file so a big PR does not overflow
20
+ # the model's context and silently truncate the reply.
21
+ _MAX_DIFF_CHARS = 60_000
22
+
23
+
24
+ def split_diff_by_file(diff: str) -> list[str]:
25
+ """Split a unified diff into one diff per file (`diff --git ...` boundaries)."""
26
+ chunks: list[str] = []
27
+ cur: list[str] = []
28
+ for line in diff.splitlines(keepends=True):
29
+ if line.startswith("diff --git ") and cur:
30
+ chunks.append("".join(cur))
31
+ cur = []
32
+ cur.append(line)
33
+ if cur:
34
+ chunks.append("".join(cur))
35
+ return chunks or ([diff] if diff.strip() else [])
36
+
37
+
38
+ def dedup_findings(findings: list[Finding]) -> list[Finding]:
39
+ seen: set = set()
40
+ out: list[Finding] = []
41
+ for f in findings:
42
+ k = (f.file, f.line, f.category, f.description)
43
+ if k not in seen:
44
+ seen.add(k)
45
+ out.append(f)
46
+ return out
47
+
48
+
49
+ def audit_diff(
50
+ diff: str,
51
+ *,
52
+ provider,
53
+ model: str,
54
+ mode: str = "standard",
55
+ max_rounds: int = 3,
56
+ filter_findings: bool = True,
57
+ finder_model: str | None = None,
58
+ challenger_model: str | None = None,
59
+ judge_model: str | None = None,
60
+ exclude_paths: tuple[str, ...] = (),
61
+ ) -> tuple[list[Finding], list[tuple[Finding, str]]]:
62
+ """Audit a diff and return (kept findings, dropped (finding, reason)).
63
+
64
+ A diff over the size budget is audited one file at a time so it does not
65
+ overflow the context. Finding categories are normalized to the rule-id set.
66
+ ``exclude_paths`` are operator-supplied path substrings to drop."""
67
+ def _run_one(d: str) -> list[Finding]:
68
+ if mode == "adversarial":
69
+ return AdversarialAuditRunner(
70
+ provider=provider, model=model,
71
+ finder_model=finder_model, challenger_model=challenger_model, judge_model=judge_model,
72
+ ).run(d, max_rounds=max_rounds).findings
73
+ return AuditRunner(provider=provider, model=model).run(d)
74
+
75
+ if len(diff) > _MAX_DIFF_CHARS:
76
+ chunks = split_diff_by_file(diff)
77
+ findings = dedup_findings([f for c in chunks for f in _run_one(c)])
78
+ else:
79
+ findings = _run_one(diff)
80
+
81
+ allowed = set(allowed_categories())
82
+ findings = [dataclasses.replace(f, category=normalize_category(f.category, allowed)) for f in findings]
83
+ if filter_findings:
84
+ return FindingsFilter(exclude_paths=exclude_paths).filter(findings)
85
+ return findings, []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.14.1
3
+ Version: 0.14.3
4
4
  Summary: AI code security review: an adversarial diff-audit engine and an agent-driven whole-repo review methodology, with security knowledge as rich rules
5
5
  Author: AISecLabs
6
6
  License-Expression: MIT
@@ -3,6 +3,8 @@ README.md
3
3
  pyproject.toml
4
4
  codejury/__init__.py
5
5
  codejury/cli.py
6
+ codejury/json_parse.py
7
+ codejury/report.py
6
8
  codejury/resources.py
7
9
  codejury.egg-info/PKG-INFO
8
10
  codejury.egg-info/SOURCES.txt
@@ -45,12 +47,10 @@ codejury/diff/debate_prompts.py
45
47
  codejury/diff/engine.py
46
48
  codejury/diff/findings_filter.py
47
49
  codejury/diff/prompts.py
48
- codejury/diff/report.py
49
50
  codejury/diff/rules.py
51
+ codejury/diff/runner.py
50
52
  codejury/domain/__init__.py
51
53
  codejury/domain/finding.py
52
- codejury/infrastructure/__init__.py
53
- codejury/infrastructure/json_parse.py
54
54
  codejury/providers/__init__.py
55
55
  codejury/providers/anthropic.py
56
56
  codejury/providers/base.py
@@ -67,12 +67,12 @@ tests/test_anthropic_provider.py
67
67
  tests/test_cli_audit.py
68
68
  tests/test_diff_debate.py
69
69
  tests/test_diff_engine.py
70
- tests/test_diff_report.py
71
70
  tests/test_json_parse.py
72
71
  tests/test_litellm_provider.py
73
72
  tests/test_openai_format.py
74
73
  tests/test_openai_provider.py
75
74
  tests/test_repo_model.py
76
75
  tests/test_repo_scaffold.py
76
+ tests/test_report.py
77
77
  tests/test_retry_provider.py
78
78
  tests/test_rules.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codejury"
3
- version = "0.14.1"
3
+ version = "0.14.3"
4
4
  description = "AI code security review: an adversarial diff-audit engine and an agent-driven whole-repo review methodology, with security knowledge as rich rules"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,4 +1,4 @@
1
- """CLI audit_diff helpers: large-diff chunking and finding de-duplication.
1
+ """Diff-audit orchestration (codejury.diff.runner) plus the thin CLI surface.
2
2
 
3
3
  A diff over the size budget is split per file and audited one file at a time so a
4
4
  big PR does not overflow the model context and silently truncate the reply; the
@@ -7,7 +7,8 @@ per-file findings are then de-duplicated.
7
7
 
8
8
  import pytest
9
9
 
10
- from codejury.cli import _dedup_findings, _split_diff_by_file, audit_diff, main
10
+ from codejury.cli import main
11
+ from codejury.diff.runner import audit_diff, dedup_findings, split_diff_by_file
11
12
  from codejury.domain.finding import Finding
12
13
  from codejury.providers.mock import MockProvider
13
14
 
@@ -16,14 +17,14 @@ _FILE_B = "diff --git a/b.py b/b.py\n@@ -0,0 +1 @@\n+y = 2\n"
16
17
 
17
18
 
18
19
  def test_split_diff_by_file():
19
- chunks = _split_diff_by_file(_FILE_A + _FILE_B)
20
+ chunks = split_diff_by_file(_FILE_A + _FILE_B)
20
21
  assert chunks == [_FILE_A, _FILE_B]
21
22
 
22
23
 
23
24
  def test_split_diff_empty_and_unbounded():
24
- assert _split_diff_by_file("") == []
25
+ assert split_diff_by_file("") == []
25
26
  # a fragment with no `diff --git` boundary is returned as one chunk
26
- assert _split_diff_by_file("just text\n") == ["just text\n"]
27
+ assert split_diff_by_file("just text\n") == ["just text\n"]
27
28
 
28
29
 
29
30
  def test_dedup_findings_collapses_identical():
@@ -31,12 +32,12 @@ def test_dedup_findings_collapses_identical():
31
32
  description="d", confidence=0.9)
32
33
  g = Finding(file="a.py", line=2, severity="HIGH", category="sql-injection",
33
34
  description="d", confidence=0.9)
34
- assert _dedup_findings([f, f, g]) == [f, g]
35
+ assert dedup_findings([f, f, g]) == [f, g]
35
36
 
36
37
 
37
38
  def test_large_diff_is_audited_per_file(monkeypatch):
38
39
  # force the chunking path on a small diff
39
- monkeypatch.setattr("codejury.cli._MAX_DIFF_CHARS", 1)
40
+ monkeypatch.setattr("codejury.diff.runner._MAX_DIFF_CHARS", 1)
40
41
  resp = ('{"findings": [{"file": "a.py", "line": 1, "severity": "HIGH", '
41
42
  '"category": "sql_injection", "description": "x", "confidence": 0.9}]}')
42
43
  provider = MockProvider(default=resp)
@@ -156,6 +156,29 @@ def test_provider_exception_degrades_rather_than_crashes():
156
156
  assert out.degraded is True
157
157
 
158
158
 
159
+ def test_judge_retry_recovers_from_a_transient_unusable_reply():
160
+ # the judge's first reply is unusable, the retry succeeds: not degraded,
161
+ # and the judge verdict is applied normally
162
+ _, out = _run([_finder([_VULN]), _challenger(), "blocked by waf", _judge([_VULN])], max_rounds=1)
163
+ assert out.degraded is False
164
+ assert [f.category for f in out.findings] == ["sql_injection"]
165
+
166
+
167
+ def test_degraded_fallback_drops_challenger_dismissed_findings():
168
+ # judge stays unusable (both the call and its retry); the degraded fallback
169
+ # must still honor the challenger's recall-safe dismissals rather than pass
170
+ # every finder finding through, which is what inflates false positives
171
+ second = {**_VULN, "line": 5, "category": "xss"}
172
+ _, out = _run(
173
+ [_finder([_VULN, second]),
174
+ _challenger(rebuttals=[{"target": "app.py:5", "verdict": "dismiss", "reason": "output is escaped"}]),
175
+ "blocked", "blocked"], # judge call + its one retry both unusable
176
+ max_rounds=1,
177
+ )
178
+ assert out.degraded is True
179
+ assert [f.category for f in out.findings] == ["sql_injection"] # the dismissed xss at app.py:5 is dropped
180
+
181
+
159
182
  def test_per_role_models_are_used():
160
183
  provider = MockProvider(responses=[_finder([]), _challenger(), _judge([])], default="{}")
161
184
  AdversarialAuditRunner(
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from codejury.infrastructure.json_parse import extract_json_object
3
+ from codejury.json_parse import extract_json_object
4
4
 
5
5
 
6
6
  def test_direct_object():
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  import jsonschema
7
7
 
8
- from codejury.diff.report import (
8
+ from codejury.report import (
9
9
  gate,
10
10
  render,
11
11
  severity_breakdown,
@@ -1,4 +0,0 @@
1
- """codejury.infrastructure: Layer 1 cross-cutting utilities, json parsing and the verdict cache.
2
-
3
- Lowest layer: depends on nothing else in codejury, used by the layers above.
4
- """
File without changes
File without changes
File without changes
File without changes