ai-cr 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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.
@@ -1,4 +1,4 @@
1
- from .cli import main
2
-
3
- if __name__ == "__main__":
4
- main()
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -59,4 +59,4 @@ def bootstrap():
59
59
  except Exception as e:
60
60
  logging.error(f"Unexpected configuration error: {e}")
61
61
  raise SystemExit(3)
62
- mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [100, 15]
62
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
ai_code_review/cli.py CHANGED
@@ -2,35 +2,33 @@ import asyncio
2
2
  import logging
3
3
  import sys
4
4
  import os
5
- import shutil
5
+ import textwrap
6
+ import tempfile
6
7
  import requests
7
8
 
8
9
  import microcore as mc
9
- import async_typer
10
10
  import typer
11
- from ai_code_review.utils import parse_refs_pair
12
11
  from git import Repo
13
12
 
14
- from .core import review
13
+ from .core import review, get_diff, filter_diff
15
14
  from .report_struct import Report
16
15
  from .constants import ENV_CONFIG_FILE
17
16
  from .bootstrap import bootstrap
18
17
  from .project_config import ProjectConfig
19
- from .utils import is_app_command_invocation
18
+ from .utils import no_subcommand, parse_refs_pair
20
19
 
21
-
22
- app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
23
- default_command_app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
24
- if sys.platform == "win32":
25
- asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
20
+ app = typer.Typer(pretty_exceptions_show_locals=False)
21
+ app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
26
22
 
27
23
 
28
24
  def main():
29
- if is_app_command_invocation(app):
30
- app()
31
- else:
25
+ if sys.platform == "win32":
26
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
27
+ if no_subcommand(app):
32
28
  bootstrap()
33
- default_command_app()
29
+ app_no_subcommand()
30
+ else:
31
+ app()
34
32
 
35
33
 
36
34
  @app.callback(invoke_without_command=True)
@@ -39,27 +37,7 @@ def cli(ctx: typer.Context):
39
37
  bootstrap()
40
38
 
41
39
 
42
- @default_command_app.async_command(name="review", help="Perform code review")
43
- @app.async_command(name="review", help="Perform code review")
44
- async def cmd_review(
45
- refs: str = typer.Argument(
46
- default=None,
47
- help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
48
- ),
49
- what: str = typer.Option(None, "--what", "-w", help="Git ref to review"),
50
- against: str = typer.Option(
51
- None,
52
- "--against", "-vs", "--vs",
53
- help="Git ref to compare against"
54
- ),
55
- filters: str = typer.Option(
56
- "", "--filter", "-f", "--filters",
57
- help="""
58
- filter reviewed files by glob / fnmatch pattern(s),
59
- e.g. 'src/**/*.py', may be comma-separated
60
- """,
61
- )
62
- ):
40
+ def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
63
41
  _what, _against = parse_refs_pair(refs)
64
42
  if _what:
65
43
  if what:
@@ -75,34 +53,103 @@ async def cmd_review(
75
53
  )
76
54
  else:
77
55
  _against = against
78
- await review(what=_what, against=_against, filters=filters)
56
+ return _what, _against
79
57
 
80
58
 
81
- @app.async_command(help="Configure LLM for local usage interactively")
82
- async def setup():
83
- mc.interactive_setup(ENV_CONFIG_FILE)
59
+ def arg_refs() -> typer.Argument:
60
+ return typer.Argument(
61
+ default=None,
62
+ help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
63
+ )
84
64
 
85
65
 
86
- @app.async_command()
87
- async def render(format: str = Report.Format.MARKDOWN):
88
- print(Report.load().render(format=format))
66
+ def arg_what() -> typer.Option:
67
+ return typer.Option(None, "--what", "-w", help="Git ref to review")
89
68
 
90
69
 
91
- @app.async_command(help="Review remote code")
92
- async def remote(url=typer.Option(), branch=typer.Option()):
93
- if os.path.exists("reviewed-repo"):
94
- shutil.rmtree("reviewed-repo")
95
- Repo.clone_from(url, branch=branch, to_path="reviewed-repo")
96
- prev_dir = os.getcwd()
97
- try:
98
- os.chdir("reviewed-repo")
99
- await review()
100
- finally:
101
- os.chdir(prev_dir)
70
+ def arg_filters() -> typer.Option:
71
+ return typer.Option(
72
+ "", "--filter", "-f", "--filters",
73
+ help="""
74
+ filter reviewed files by glob / fnmatch pattern(s),
75
+ e.g. 'src/**/*.py', may be comma-separated
76
+ """,
77
+ )
102
78
 
103
79
 
104
- @app.async_command(help="Leave a GitHub PR comment with the review.")
105
- async def github_comment(
80
+ def arg_out() -> typer.Option:
81
+ return typer.Option(
82
+ None,
83
+ "--out", "-o", "--output",
84
+ help="Output folder for the code review report"
85
+ )
86
+
87
+
88
+ def arg_against() -> typer.Option:
89
+ return typer.Option(
90
+ None,
91
+ "--against", "-vs", "--vs",
92
+ help="Git ref to compare against"
93
+ )
94
+
95
+
96
+ @app_no_subcommand.command(name="review", help="Perform code review")
97
+ @app.command(name="review", help="Perform code review")
98
+ def cmd_review(
99
+ refs: str = arg_refs(),
100
+ what: str = arg_what(),
101
+ against: str = arg_against(),
102
+ filters: str = arg_filters(),
103
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
104
+ out: str = arg_out()
105
+ ):
106
+ _what, _against = args_to_target(refs, what, against)
107
+ asyncio.run(review(
108
+ what=_what,
109
+ against=_against,
110
+ filters=filters,
111
+ use_merge_base=merge_base,
112
+ out_folder=out,
113
+ ))
114
+
115
+
116
+ @app.command(help="Configure LLM for local usage interactively")
117
+ def setup():
118
+ mc.interactive_setup(ENV_CONFIG_FILE)
119
+
120
+
121
+ @app.command()
122
+ def render(format: str = Report.Format.MARKDOWN):
123
+ print(Report.load().render(format=format))
124
+
125
+
126
+ @app.command(help="Review remote code")
127
+ def remote(
128
+ url: str = typer.Argument(..., help="Git repository URL"),
129
+ refs: str = arg_refs(),
130
+ what: str = arg_what(),
131
+ against: str = arg_against(),
132
+ filters: str = arg_filters(),
133
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
134
+ out: str = arg_out()
135
+ ):
136
+ _what, _against = args_to_target(refs, what, against)
137
+ with tempfile.TemporaryDirectory() as temp_dir:
138
+ logging.info(f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ...")
139
+ repo = Repo.clone_from(url, branch=_what, to_path=temp_dir)
140
+ asyncio.run(review(
141
+ repo=repo,
142
+ what=_what,
143
+ against=_against,
144
+ filters=filters,
145
+ use_merge_base=merge_base,
146
+ out_folder=out or '.',
147
+ ))
148
+ repo.close()
149
+
150
+
151
+ @app.command(help="Leave a GitHub PR comment with the review.")
152
+ def github_comment(
106
153
  token: str = typer.Option(
107
154
  os.environ.get("GITHUB_TOKEN", ""), help="GitHub token (or set GITHUB_TOKEN env var)"
108
155
  ),
@@ -110,7 +157,7 @@ async def github_comment(
110
157
  """
111
158
  Leaves a comment with the review on the current GitHub pull request.
112
159
  """
113
- file = "code-review-report.txt"
160
+ file = "code-review-report.md"
114
161
  if not os.path.exists(file):
115
162
  print(f"Review file not found: {file}")
116
163
  raise typer.Exit(4)
@@ -155,3 +202,35 @@ async def github_comment(
155
202
  else:
156
203
  logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
157
204
  raise typer.Exit(5)
205
+
206
+
207
+ @app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
208
+ def files(
209
+ refs: str = arg_refs(),
210
+ what: str = arg_what(),
211
+ against: str = arg_against(),
212
+ filters: str = arg_filters(),
213
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
214
+ diff: bool = typer.Option(default=False, help="Show diff content")
215
+ ):
216
+ _what, _against = args_to_target(refs, what, against)
217
+ repo = Repo(".")
218
+ patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
219
+ patch_set = filter_diff(patch_set, filters)
220
+ print(
221
+ f"Changed files: "
222
+ f"{mc.ui.green(_what or 'INDEX')} vs "
223
+ f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
224
+ f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
225
+ )
226
+
227
+ for patch in patch_set:
228
+ if patch.is_added_file:
229
+ color = mc.ui.green
230
+ elif patch.is_removed_file:
231
+ color = mc.ui.red
232
+ else:
233
+ color = mc.ui.blue
234
+ print(f"- {color(patch.path)}")
235
+ if diff:
236
+ print(mc.ui.gray(textwrap.indent(str(patch), " ")))
ai_code_review/core.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import fnmatch
2
2
  import logging
3
+ from os import PathLike
3
4
  from typing import Iterable
5
+ from pathlib import Path
4
6
 
5
7
  import microcore as mc
6
8
  from git import Repo
@@ -9,22 +11,76 @@ from unidiff.constants import DEV_NULL
9
11
 
10
12
  from .project_config import ProjectConfig
11
13
  from .report_struct import Report
14
+ from .constants import JSON_REPORT_FILE_NAME
15
+
16
+
17
+ def is_binary_file(repo: Repo, file_path: str) -> bool:
18
+ """
19
+ Check if a file is binary by attempting to read it as text.
20
+ Returns True if the file is binary, False otherwise.
21
+ """
22
+ try:
23
+ # Attempt to read the file content from the repository tree
24
+ content = repo.tree()[file_path].data_stream.read()
25
+ # Try decoding as UTF-8; if it fails, it's likely binary
26
+ content.decode("utf-8")
27
+ return False
28
+ except (UnicodeDecodeError, KeyError):
29
+ return True
30
+ except Exception as e:
31
+ logging.warning(f"Error checking if file {file_path} is binary: {e}")
32
+ return True # Conservatively treat errors as binary to avoid issues
12
33
 
13
34
 
14
35
  def get_diff(
15
36
  repo: Repo = None,
16
37
  what: str = None,
17
- against: str = None
38
+ against: str = None,
39
+ use_merge_base: bool = True,
18
40
  ) -> PatchSet | list[PatchedFile]:
19
41
  repo = repo or Repo(".")
20
42
  if not against:
21
43
  against = repo.remotes.origin.refs.HEAD.reference.name # origin/main
22
44
  if not what:
23
45
  what = None # working copy
24
- logging.info(f"Reviewing {mc.ui.green(what or 'working copy')} vs {mc.ui.yellow(against)}")
46
+ if use_merge_base:
47
+ if what is None:
48
+ try:
49
+ current_ref = repo.active_branch.name
50
+ except TypeError:
51
+ # In detached HEAD state, use HEAD directly
52
+ current_ref = "HEAD"
53
+ logging.info(
54
+ "Detected detached HEAD state, using HEAD as current reference"
55
+ )
56
+ else:
57
+ current_ref = what
58
+ merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
59
+ against = merge_base.hexsha
60
+ logging.info(
61
+ f"Using merge base: {mc.ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
62
+ )
63
+ logging.info(
64
+ f"Making diff: {mc.ui.green(what or 'INDEX')} vs {mc.ui.yellow(against)}"
65
+ )
25
66
  diff_content = repo.git.diff(against, what)
26
67
  diff = PatchSet.from_string(diff_content)
27
- return diff
68
+ diff = PatchSet.from_string(diff_content)
69
+
70
+ # Filter out binary files
71
+ non_binary_diff = PatchSet([])
72
+ for patched_file in diff:
73
+ # Check if the file is binary using the source or target file path
74
+ file_path = (
75
+ patched_file.target_file
76
+ if patched_file.target_file != DEV_NULL
77
+ else patched_file.source_file
78
+ )
79
+ if file_path == DEV_NULL or is_binary_file(repo, file_path.lstrip("b/")):
80
+ logging.info(f"Skipping binary file: {patched_file.path}")
81
+ continue
82
+ non_binary_diff.append(patched_file)
83
+ return non_binary_diff
28
84
 
29
85
 
30
86
  def filter_diff(
@@ -33,7 +89,6 @@ def filter_diff(
33
89
  """
34
90
  Filter the diff files by the given fnmatch filters.
35
91
  """
36
- print([f.path for f in patch_set])
37
92
  assert isinstance(filters, (list, str))
38
93
  if not isinstance(filters, list):
39
94
  filters = [f.strip() for f in filters.split(",") if f.strip()]
@@ -44,7 +99,6 @@ def filter_diff(
44
99
  for file in patch_set
45
100
  if any(fnmatch.fnmatch(file.path, pattern) for pattern in filters)
46
101
  ]
47
- print([f.path for f in files])
48
102
  return files
49
103
 
50
104
 
@@ -61,22 +115,32 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None) -> str:
61
115
 
62
116
 
63
117
  def make_cr_summary(cfg: ProjectConfig, report: Report, diff):
64
- return mc.prompt(
65
- cfg.summary_prompt,
66
- diff=mc.tokenizing.fit_to_token_size(diff, cfg.max_code_tokens)[0],
67
- issues=report.issues,
68
- **cfg.prompt_vars,
69
- ).to_llm() if cfg.summary_prompt else ""
118
+ return (
119
+ mc.prompt(
120
+ cfg.summary_prompt,
121
+ diff=mc.tokenizing.fit_to_token_size(diff, cfg.max_code_tokens)[0],
122
+ issues=report.issues,
123
+ **cfg.prompt_vars,
124
+ ).to_llm()
125
+ if cfg.summary_prompt
126
+ else ""
127
+ )
70
128
 
71
129
 
72
130
  async def review(
131
+ repo: Repo = None,
73
132
  what: str = None,
74
133
  against: str = None,
75
- filters: str | list[str] = ""
134
+ filters: str | list[str] = "",
135
+ use_merge_base: bool = True,
136
+ out_folder: str | PathLike | None = None,
76
137
  ):
77
138
  cfg = ProjectConfig.load()
78
- repo = Repo(".")
79
- diff = get_diff(repo=repo, what=what, against=against)
139
+ repo = repo or Repo(".")
140
+ out_folder = Path(out_folder or repo.working_tree_dir)
141
+ diff = get_diff(
142
+ repo=repo, what=what, against=against, use_merge_base=use_merge_base
143
+ )
80
144
  diff = filter_diff(diff, filters)
81
145
  if not diff:
82
146
  logging.error("Nothing to review")
@@ -114,12 +178,14 @@ async def review(
114
178
  if lines[file]:
115
179
  f_lines = [""] + lines[file].splitlines()
116
180
  i["affected_code"] = "\n".join(
117
- f_lines[i["start_line"]: i["end_line"]+1]
181
+ f_lines[i["start_line"]: i["end_line"] + 1]
118
182
  )
119
183
  exec(cfg.post_process, {"mc": mc, **locals()})
184
+ out_folder.mkdir(parents=True, exist_ok=True)
120
185
  report = Report(issues=issues, number_of_processed_files=len(diff))
121
186
  report.summary = make_cr_summary(cfg, report, diff)
122
- report.save()
187
+ report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
123
188
  report_text = report.render(cfg, Report.Format.MARKDOWN)
124
189
  print(mc.ui.yellow(report_text))
125
- open("code-review-report.txt", "w", encoding="utf-8").write(report_text)
190
+ text_report_path = out_folder / "code-review-report.md"
191
+ text_report_path.write_text(report_text, encoding="utf-8")
ai_code_review/utils.py CHANGED
@@ -92,11 +92,11 @@ def is_running_in_github_action():
92
92
  return os.getenv("GITHUB_ACTIONS") == "true"
93
93
 
94
94
 
95
- def is_app_command_invocation(app: typer.Typer) -> bool:
95
+ def no_subcommand(app: typer.Typer) -> bool:
96
96
  """
97
97
  Checks if the current script is being invoked as a command in a target Typer application.
98
98
  """
99
- return (
99
+ return not (
100
100
  (first_arg := next((a for a in sys.argv[1:] if not a.startswith('-')), None))
101
101
  and first_arg in (
102
102
  cmd.name or cmd.callback.__name__.replace('_', '-')
@@ -106,7 +106,7 @@ def is_app_command_invocation(app: typer.Typer) -> bool:
106
106
  )
107
107
 
108
108
 
109
- def parse_refs_pair(refs: str):
109
+ def parse_refs_pair(refs: str) -> tuple[str | None, str | None]:
110
110
  SEPARATOR = '..'
111
111
  if not refs:
112
112
  return None, None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-cr
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: LLM-agnostic GitHub AI Code Review Tool with integration to GitHub actions
5
5
  License: MIT
6
6
  Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
@@ -15,13 +15,12 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Software Development
18
- Requires-Dist: GitPython (==3.1.44)
19
- Requires-Dist: ai-microcore (==4.0.0.dev18)
20
- Requires-Dist: anthropic (==0.52.2)
21
- Requires-Dist: async-typer (==0.1.8)
22
- Requires-Dist: google-generativeai (==0.8.5)
23
- Requires-Dist: typer (==0.9.4)
24
- Requires-Dist: unidiff (==0.7.5)
18
+ Requires-Dist: GitPython (>=3.1.44,<4.0.0)
19
+ Requires-Dist: ai-microcore (==4.0.0.dev19)
20
+ Requires-Dist: anthropic (>=0.52.2,<0.53.0)
21
+ Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
22
+ Requires-Dist: typer (>=0.16.0,<0.17.0)
23
+ Requires-Dist: unidiff (>=0.7.5,<0.8.0)
25
24
  Project-URL: Homepage, https://github.com/Nayjest/github-ai-code-review
26
25
  Project-URL: Repository, https://github.com/Nayjest/github-ai-code-review
27
26
  Description-Content-Type: text/markdown
@@ -30,6 +29,7 @@ Description-Content-Type: text/markdown
30
29
  <a href="https://pypi.org/project/ai-code-review/" target="_blank"><img src="https://badge.fury.io/py/ai-code-review.svg" alt="PYPI Release"></a>
31
30
  <a href="https://github.com/Nayjest/ai-code-review/actions/workflows/code-style.yml" target="_blank"><img src="https://github.com/Nayjest/ai-code-review/actions/workflows/code-style.yml/badge.svg" alt="Pylint"></a>
32
31
  <a href="https://github.com/Nayjest/ai-code-review/actions/workflows/tests.yml" target="_blank"><img src="https://github.com/Nayjest/ai-code-review/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
32
+ <img src="https://github.com/Nayjest/ai-code-review/blob/main/coverage.svg" alt="Code Coverage">
33
33
  <a href="https://github.com/Nayjest/ai-code-review/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/static/v1?label=license&message=MIT&color=d08aff" alt="License"></a>
34
34
  </p>
35
35
 
@@ -91,7 +91,8 @@ jobs:
91
91
  > ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
92
92
 
93
93
  💪 Done!
94
- PRs to your repository will now receive AI code reviews automatically. ✨
94
+ PRs to your repository will now receive AI code reviews automatically. ✨
95
+ See [GitHub Setup Guide](https://github.com/Nayjest/ai-code-review/blob/main/documentation/github_setup.md) for more details.
95
96
 
96
97
  ### 2. Run Code Analysis Locally
97
98
 
@@ -0,0 +1,15 @@
1
+ ai_code_review/.ai-code-review.toml,sha256=z-3iliCvkkpdAr9l_yUyM-IbgRm9hbAA3CIV6ocuurY,10378
2
+ ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ai_code_review/__main__.py,sha256=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
4
+ ai_code_review/bootstrap.py,sha256=jqioR_UtTsn5nXezmjMLU3aB8tzlVz73ZRBk33ud5F4,2336
5
+ ai_code_review/cli.py,sha256=1wrF-s6U_f8BjwCl70G7IXIBEhmMUbQDvij5AMDxQ5M,7318
6
+ ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
7
+ ai_code_review/core.py,sha256=BJNs4ZER2-bMulXY2apY6B6hI0nvRCOrLqsJ7L8Bizc,6693
8
+ ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
9
+ ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
10
+ ai_code_review/utils.py,sha256=vlzU3M89qK6_mVkBMnppZaOFsXddVsIBVdfmbN3cxDY,2939
11
+ ai_cr-0.6.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
12
+ ai_cr-0.6.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
13
+ ai_cr-0.6.0.dist-info/METADATA,sha256=poIlyrRUJQEKwRN7JIgKGOjgL1DR291tw6jibTi4EMs,5835
14
+ ai_cr-0.6.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
15
+ ai_cr-0.6.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- ai_code_review/.ai-code-review.toml,sha256=z-3iliCvkkpdAr9l_yUyM-IbgRm9hbAA3CIV6ocuurY,10378
2
- ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- ai_code_review/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
4
- ai_code_review/bootstrap.py,sha256=b9BlhRgliwsjmWRCCZcsTivsOw9AbXOf8_AZYjQTx-c,2336
5
- ai_code_review/cli.py,sha256=L1utFMHs4D9HVcuJ1VC96PEvP6q26UHsfCz_Lq49VMg,4893
6
- ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
7
- ai_code_review/core.py,sha256=WMGU2zwZTJ1P6PkUId-RJP7KHQbzB31GKSnI5wsedcA,4177
8
- ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
9
- ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
10
- ai_code_review/utils.py,sha256=UzFp2ShNzQvsydxJ98bdr_j2rBUHcm62pPtl6hBuZD4,2914
11
- ai_cr-0.5.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
12
- ai_cr-0.5.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
13
- ai_cr-0.5.0.dist-info/METADATA,sha256=k3FaB6nbVV0xbgZYPg_VQ6zMauB65Xy6CLWI4qFOMF0,5609
14
- ai_cr-0.5.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
15
- ai_cr-0.5.0.dist-info/RECORD,,
File without changes
File without changes