code-review-ai-cli 1.3.4__tar.gz → 1.3.6__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 (28) hide show
  1. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/PKG-INFO +7 -3
  2. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/README.md +6 -2
  3. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/PKG-INFO +7 -3
  4. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/SOURCES.txt +2 -0
  5. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/pyproject.toml +1 -1
  6. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/ai_review.py +0 -1
  7. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/config.py +0 -2
  8. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/git_utils.py +0 -1
  9. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/llm_client.py +119 -223
  10. code_review_ai_cli-1.3.6/src/prompt_utils.py +56 -0
  11. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/prompts/config.yaml.template +0 -3
  12. code_review_ai_cli-1.3.6/src/prompts/review_prompt.md.template +91 -0
  13. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_config.py +8 -2
  14. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_llm_client.py +163 -69
  15. code_review_ai_cli-1.3.6/tests/test_prompt_utils.py +129 -0
  16. code_review_ai_cli-1.3.4/src/prompts/review_prompt.md.template +0 -2
  17. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
  18. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
  19. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/requires.txt +0 -0
  20. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/top_level.txt +0 -0
  21. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/setup.cfg +0 -0
  22. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/__init__.py +0 -0
  23. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/formatter.py +0 -0
  24. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/tfs_client.py +0 -0
  25. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_ai_review.py +0 -0
  26. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_formatter.py +0 -0
  27. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_git_utils.py +0 -0
  28. {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_tfs_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-review-ai-cli
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
5
5
  License: MIT
6
6
  Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
@@ -126,7 +126,6 @@ tfs:
126
126
  # ca_bundle: C:/certs/corporate-root-ca.pem
127
127
 
128
128
  review:
129
- language: pt
130
129
  verbosity: detailed
131
130
  scope: diff_only
132
131
  custom_prompt_file: review_prompt.md
@@ -205,7 +204,12 @@ Edit it to tailor the review to your team:
205
204
  - Include business/architecture context
206
205
  - Add examples of good/bad comments
207
206
 
208
- The path is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
207
+ **Rules can be scoped** to specific file types using language tags. During a review, the AI detects the files changed in the diff, identifies their extensions, and only loads:
208
+
209
+ - Rules marked with `<!-- lang: all -->` for all files.
210
+ - Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
211
+
212
+ The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
209
213
 
210
214
  ```yaml
211
215
  review:
@@ -89,7 +89,6 @@ tfs:
89
89
  # ca_bundle: C:/certs/corporate-root-ca.pem
90
90
 
91
91
  review:
92
- language: pt
93
92
  verbosity: detailed
94
93
  scope: diff_only
95
94
  custom_prompt_file: review_prompt.md
@@ -168,7 +167,12 @@ Edit it to tailor the review to your team:
168
167
  - Include business/architecture context
169
168
  - Add examples of good/bad comments
170
169
 
171
- The path is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
170
+ **Rules can be scoped** to specific file types using language tags. During a review, the AI detects the files changed in the diff, identifies their extensions, and only loads:
171
+
172
+ - Rules marked with `<!-- lang: all -->` for all files.
173
+ - Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
174
+
175
+ The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
172
176
 
173
177
  ```yaml
174
178
  review:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-review-ai-cli
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
5
5
  License: MIT
6
6
  Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
@@ -126,7 +126,6 @@ tfs:
126
126
  # ca_bundle: C:/certs/corporate-root-ca.pem
127
127
 
128
128
  review:
129
- language: pt
130
129
  verbosity: detailed
131
130
  scope: diff_only
132
131
  custom_prompt_file: review_prompt.md
@@ -205,7 +204,12 @@ Edit it to tailor the review to your team:
205
204
  - Include business/architecture context
206
205
  - Add examples of good/bad comments
207
206
 
208
- The path is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
207
+ **Rules can be scoped** to specific file types using language tags. During a review, the AI detects the files changed in the diff, identifies their extensions, and only loads:
208
+
209
+ - Rules marked with `<!-- lang: all -->` for all files.
210
+ - Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
211
+
212
+ The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
209
213
 
210
214
  ```yaml
211
215
  review:
@@ -12,6 +12,7 @@ src/config.py
12
12
  src/formatter.py
13
13
  src/git_utils.py
14
14
  src/llm_client.py
15
+ src/prompt_utils.py
15
16
  src/tfs_client.py
16
17
  src/prompts/config.yaml.template
17
18
  src/prompts/review_prompt.md.template
@@ -20,4 +21,5 @@ tests/test_config.py
20
21
  tests/test_formatter.py
21
22
  tests/test_git_utils.py
22
23
  tests/test_llm_client.py
24
+ tests/test_prompt_utils.py
23
25
  tests/test_tfs_client.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-review-ai-cli"
7
- version = "1.3.4"
7
+ version = "1.3.6"
8
8
  description = "Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -858,7 +858,6 @@ def _show_config(config: ReviewConfig) -> None:
858
858
  print(f" {c.CYAN}API Key:{c.RESET} {'✅ Configured' if has_effective_key else '❌ Not configured'}")
859
859
  print(f" {c.CYAN}Temperature:{c.RESET} {config.temperature}")
860
860
  print(f" {c.CYAN}Max Tokens:{c.RESET} {config.max_tokens}")
861
- print(f" {c.CYAN}Language:{c.RESET} {config.review_language}")
862
861
  print(f" {c.CYAN}Verbosity:{c.RESET} {config.verbosity}")
863
862
  print(f" {c.CYAN}Format:{c.RESET} {config.output_format}")
864
863
  print(f"\n {c.CYAN}TFS URL:{c.RESET} {config.tfs_base_url or '(not configured)'}")
@@ -85,7 +85,6 @@ class ReviewConfig:
85
85
  tfs_repository: str = "" # Default repository (empty = all)
86
86
 
87
87
  # --- Review -------------------------------------------------------
88
- review_language: str = "pt" # Review language (pt/en)
89
88
  verbosity: str = "detailed" # "quick" | "detailed" | "security"
90
89
  review_scope: str = "diff_only" # "diff_only" | "full_code"
91
90
  max_diff_files: int = 50 # Max diff files sent to LLM
@@ -211,7 +210,6 @@ class ReviewConfig:
211
210
  "tfs_ca_bundle": ("tfs", "ca_bundle"),
212
211
  "tfs_repository": ("tfs", "repository"),
213
212
  # Review
214
- "review_language": ("review", "language"),
215
213
  "verbosity": ("review", "verbosity"),
216
214
  "review_scope": ("review", "scope"),
217
215
  "max_diff_files": ("review", "max_diff_files"),
@@ -14,7 +14,6 @@ import subprocess
14
14
  import os
15
15
  from typing import Optional
16
16
 
17
-
18
17
  class GitError(Exception):
19
18
  """Exception for Git-related errors."""
20
19
  pass
@@ -17,6 +17,7 @@ import json
17
17
  import os
18
18
 
19
19
  from .config import ReviewConfig
20
+ from .prompt_utils import detect_langs, filter_prompt_by_langs
20
21
 
21
22
 
22
23
  class LLMError(Exception):
@@ -26,209 +27,124 @@ class LLMError(Exception):
26
27
  # ---------------------------------------------------------------------------
27
28
  # System Prompts
28
29
  # ---------------------------------------------------------------------------
30
+ # Maps each verbosity level ("quick", "detailed", "security") to its
31
+ # English-only system prompt.
32
+ #
33
+ # Type:
34
+ # dict[str, str] — flat mapping of verbosity -> prompt string. There is
35
+ # no per-language nesting; each value is a plain ``str``.
36
+ #
37
+ # See Also:
38
+ # get_system_prompt: Resolves a prompt for a given verbosity, with a
39
+ # fallback to "detailed" when the key is not recognised.
29
40
  SYSTEM_PROMPTS = {
30
- "quick": {
31
- "pt": (
32
- "És um code reviewer senior experiente. Analisa o diff de código fornecido "
33
- "e dá um review CONCISO e direto. Foca-te nos problemas mais críticos:\n"
34
- "- Bugs e erros lógicos\n"
35
- "- Problemas de segurança\n"
36
- "- Problemas de performance graves\n\n"
37
- "Formato: Lista de bullet points com o ficheiro e linha quando possível. "
38
- ),
39
- "en": (
40
- "You are an experienced Senior Code Reviewer. Analyze the provided code diff "
41
- "and give a CONCISE review. Focus on critical issues:\n"
42
- "- Bugs and logic errors\n"
43
- "- Security issues\n"
44
- "- Major performance problems\n\n"
45
- "Format: Bullet points with file and line when possible. "
46
- ),
47
- },
48
- "detailed": {
49
- "pt": (
50
- "És um code reviewer senior experiente. Analisa o diff de código "
51
- "fornecido e retorna apenas comentários inline.\n\n"
52
- "Formato de output — para cada problema encontrado, escreve exatamente:\n"
53
- "- Linha <número_linha>: <descrição do problema> \n\n"
54
- "Regras:\n"
55
- "- Reporta APENAS problemas específicos associados a uma linha ou bloco concreto de código alterado.\n"
56
- "- NÃO produzas secções de sumário (ex: 'Bugs Potenciais', 'Visão Geral de Segurança').\n"
57
- "- NÃO produzas parágrafos introdutórios ou de fecho.\n"
58
- "- Se não encontrares problemas, escreve apenas: Nenhum problema encontrado.\n"
59
- "- Sê específico, objetivo e conciso. Responde em português."
60
- ),
61
- "en": (
62
- "You are an experienced Senior Code Reviewer. Analyze the provided code and return only inline comments.\n\n"
63
- "Output format — for each issue found, output exactly:\n"
64
- "- Line <line_number>: <issue description>\n\n"
65
- "Rules:\n"
66
- "- Report ONLY specific issues tied to a concrete line or block of changed code.\n"
67
- "- Do NOT produce summary sections (e.g. 'Potential Bugs', 'Security Overview').\n"
68
- "- Do NOT produce introductory or closing paragraphs.\n"
69
- "- If no issues are found, output only: No issues found.\n"
70
- "- Be specific, actionable, and concise."
71
- ),
72
- },
73
- "security": {
74
- "pt": (
75
- "És um especialista em segurança de aplicações. Analisa o diff "
76
- "de código fornecido com foco EXCLUSIVO em segurança.\n\n"
77
- "Procura por:\n"
78
- "- SQL Injection\n"
79
- "- Cross-Site Scripting (XSS)\n"
80
- "- Cross-Site Request Forgery (CSRF)\n"
81
- "- Credenciais hardcoded ou secrets expostos\n"
82
- "- Vulnerabilidades de autenticação/autorização\n"
83
- "- Insecure deserialization\n"
84
- "- Path traversal\n"
85
- "- Command injection\n"
86
- "- Dependências com vulnerabilidades conhecidas\n"
87
- "- Logging de informação sensível\n"
88
- "- Configurações inseguras\n\n"
89
- "Fornece recomendações de correção para cada problema. "
90
- "Responde em português."
91
- ),
92
- "en": (
93
- "You are an application security. Analyze the "
94
- "provided code diff with EXCLUSIVE focus on security.\n\n"
95
- "Look for:\n"
96
- "- SQL Injection\n"
97
- "- Cross-Site Scripting (XSS)\n"
98
- "- Cross-Site Request Forgery (CSRF)\n"
99
- "- Hardcoded credentials or exposed secrets\n"
100
- "- Authentication/authorization vulnerabilities\n"
101
- "- Insecure deserialization\n"
102
- "- Path traversal\n"
103
- "- Command injection\n"
104
- "- Dependencies with known vulnerabilities\n"
105
- "- Logging of sensitive information\n"
106
- "- Insecure configurations\n\n"
107
- "Provide fix recommendations for each issue."
108
- ),
109
- },
110
- }
111
-
112
- # Special prompt for PR review with a combined narrative summary + structured comments
113
- PR_COMMENT_PROMPT = {
114
- "pt": (
115
- "Analisa o diff de código de um Pull Request e retorna a tua resposta num único objeto JSON "
116
- "com dois campos:\n"
117
- '- "summary": um resumo narrativo do review, em texto (string). Se não encontrares problemas, '
118
- 'escreve algo como "Nenhum problema encontrado."\n'
119
- '- "comments": um array de objetos, um por cada problema encontrado, com:\n'
120
- ' - "file": caminho do ficheiro (ex: "src/auth.py")\n'
121
- ' - "line": número da linha no diff (inteiro, ou 0 se geral)\n'
122
- ' - "type": tipo de issue ("bug", "security", "performance", "style", "suggestion", "praise")\n'
123
- ' - "comment": descrição direta do problema em português, sem saudações e sem emojis\n'
124
- ' - "suggestion": sugestão de correção (opcional, string vazia se não aplicável)\n'
125
- ' - "reference": fonte ou referência para o problema (URL de documentação, padrão ou princípio). '
126
- 'Importante: incluir SEMPRE uma referência relevante.\n\n'
127
- "No campo 'comment', escreve de forma objetiva e curta. "
128
- "Não uses introduções como 'Olá' ou 'Como code reviewer sénior'.\n"
129
- "No campo 'reference', inclui uma fonte confiável, padrão ou link para documentação relevante.\n\n"
130
- "Responde APENAS com um JSON object válido. Exemplo:\n"
131
- '{\n'
132
- ' "summary": "Resumo narrativo curto do review...",\n'
133
- ' "comments": [\n'
134
- ' {\n'
135
- ' "file": "src/auth.py",\n'
136
- ' "line": 42,\n'
137
- ' "type": "security",\n'
138
- ' "comment": "Password armazenada em texto simples sem hashing",\n'
139
- ' "suggestion": "Usar bcrypt ou argon2 para hash de passwords",\n'
140
- ' "reference": "OWASP - Password Storage Cheat Sheet"\n'
141
- ' }\n'
142
- ' ]\n'
143
- '}\n\n'
41
+ "quick": (
42
+ "You are an experienced Senior Code Reviewer. Analyze the provided code diff "
43
+ "and give a CONCISE review. Focus on critical issues:\n"
44
+ "- Bugs and logic errors\n"
45
+ "- Security issues\n"
46
+ "- Major performance problems\n\n"
47
+ "Format: Bullet points with file and line when possible. "
48
+ ),
49
+ "detailed": (
50
+ "You are an experienced Senior Code Reviewer. Analyze the provided code and return only inline comments.\n\n"
51
+ "Output format for each issue found, output exactly:\n"
52
+ "- Line <line_number>: <issue description>\n\n"
53
+ "Rules:\n"
54
+ "- Report ONLY specific issues tied to a concrete line or block of changed code.\n"
55
+ "- Do NOT produce summary sections (e.g. 'Potential Bugs', 'Security Overview').\n"
56
+ "- Do NOT produce introductory or closing paragraphs.\n"
57
+ "- If no issues are found, output only: No issues found.\n"
58
+ "- Be specific, actionable, and concise."
144
59
  ),
145
- "en": (
146
- "Analyze the Pull Request code diff and return your response as a single JSON object with two "
147
- "fields:\n"
148
- '- "summary": a narrative text summary of the review (string). If no issues are found, write '
149
- 'something like "No issues found."\n'
150
- '- "comments": an array of objects, one per issue found, with:\n'
151
- ' - "file": file path (e.g., "src/auth.py")\n'
152
- ' - "line": line number in diff (integer, or 0 if general)\n'
153
- ' - "type": issue type ("bug", "security", "performance", "style", "suggestion", "praise")\n'
154
- ' - "comment": direct description of the issue, with no greetings and no emojis\n'
155
- ' - "suggestion": fix suggestion (optional, empty string if not applicable)\n'
156
- ' - "reference": source or reference for the issue (e.g., "OWASP Top 10", "PEP 8", documentation '
157
- 'URL, standard or principle). Important: ALWAYS include a relevant reference.\n\n'
158
- "In 'comment', use a short and objective tone. "
159
- "Do not include intros like 'Hello' or 'As a senior reviewer'.\n"
160
- "In 'reference', include a trusted source, standard or link to relevant documentation.\n\n"
161
- "Respond ONLY with a valid JSON object. Example:\n"
162
- '{\n'
163
- ' "summary": "Short narrative summary of the review...",\n'
164
- ' "comments": [\n'
165
- ' {\n'
166
- ' "file": "src/auth.py",\n'
167
- ' "line": 42,\n'
168
- ' "type": "security",\n'
169
- ' "comment": "Password stored in plain text without hashing",\n'
170
- ' "suggestion": "Use bcrypt or argon2 to hash passwords",\n'
171
- ' "reference": "OWASP - Password Storage Cheat Sheet"\n'
172
- ' }\n'
173
- ' ]\n'
174
- '}\n\n'
60
+ "security": (
61
+ "You are an application security. Analyze the "
62
+ "provided code diff with EXCLUSIVE focus on security.\n\n"
63
+ "Look for:\n"
64
+ "- SQL Injection\n"
65
+ "- Cross-Site Scripting (XSS)\n"
66
+ "- Cross-Site Request Forgery (CSRF)\n"
67
+ "- Hardcoded credentials or exposed secrets\n"
68
+ "- Authentication/authorization vulnerabilities\n"
69
+ "- Insecure deserialization\n"
70
+ "- Path traversal\n"
71
+ "- Command injection\n"
72
+ "- Dependencies with known vulnerabilities\n"
73
+ "- Logging of sensitive information\n"
74
+ "- Insecure configurations\n\n"
75
+ "Provide fix recommendations for each issue."
175
76
  ),
176
77
  }
177
78
 
178
-
179
- def get_system_prompt(verbosity: str, language: str) -> str:
180
- """Returns the system prompt for the given verbosity level and language.
79
+ # Special prompt for PR review with a combined narrative summary + structured comments.
80
+ #
81
+ # Type:
82
+ # str — a single English-only prompt (not a per-language dict) instructing
83
+ # the LLM to respond with one JSON object containing a "summary" string
84
+ # and a "comments" array (each with file, line, type, comment,
85
+ # suggestion, and reference fields).
86
+ #
87
+ # See Also:
88
+ # LLMClient.review_pr: Combines this prompt with the verbosity system
89
+ # prompt (from get_system_prompt) and scope guidance (from
90
+ # get_scope_guidance) to build the final system prompt sent to the LLM.
91
+ PR_COMMENT_PROMPT = (
92
+ "Analyze the Pull Request code diff and return your response as a single JSON object with two "
93
+ "fields:\n"
94
+ '- "summary": a narrative text summary of the review (string). If no issues are found, write '
95
+ 'something like "No issues found."\n'
96
+ '- "comments": an array of objects, one per issue found, with:\n'
97
+ ' - "file": file path (e.g., "src/auth.py")\n'
98
+ ' - "line": line number in diff (integer, or 0 if general)\n'
99
+ ' - "type": issue type ("bug", "security", "performance", "style", "suggestion", "praise")\n'
100
+ ' - "comment": direct description of the issue, with no greetings and no emojis\n'
101
+ ' - "suggestion": fix suggestion (optional, empty string if not applicable)\n'
102
+ ' - "reference": source or reference for the issue (e.g., "OWASP Top 10", "PEP 8", documentation '
103
+ 'URL, standard or principle). Important: ALWAYS include a relevant reference.\n\n'
104
+ "In 'comment', use a short and objective tone. "
105
+ "Do not include intros like 'Hello' or 'As a senior reviewer'.\n"
106
+ "In 'reference', include a trusted source, standard or link to relevant documentation.\n\n"
107
+ "Respond ONLY with a valid JSON object. Example:\n"
108
+ '{\n'
109
+ ' "summary": "Short narrative summary of the review...",\n'
110
+ ' "comments": [\n'
111
+ ' {\n'
112
+ ' "file": "src/auth.py",\n'
113
+ ' "line": 42,\n'
114
+ ' "type": "security",\n'
115
+ ' "comment": "Password stored in plain text without hashing",\n'
116
+ ' "suggestion": "Use bcrypt or argon2 to hash passwords",\n'
117
+ ' "reference": "OWASP - Password Storage Cheat Sheet"\n'
118
+ ' }\n'
119
+ ' ]\n'
120
+ '}\n\n'
121
+ )
122
+
123
+
124
+ def get_system_prompt(verbosity: str) -> str:
125
+ """Returns the system prompt for the given verbosity level.
181
126
 
182
127
  Selects a prompt from ``SYSTEM_PROMPTS`` keyed by *verbosity*. If the
183
128
  requested verbosity is not found, the function falls back to
184
- ``"detailed"``. Within the selected prompt group, the language is
185
- resolved by *language*; if the language key is absent, ``"pt"``
186
- (Portuguese) is used as the default.
129
+ ``"detailed"``.
187
130
 
188
131
  The ``"detailed"`` prompt instructs the LLM to return **only** inline
189
132
  comments in the format ``- Line <n>: <description>``,
190
133
  prohibiting summary sections and introductory/closing paragraphs.
191
134
  When no issues are found the expected output is ``No issues found.``
192
- (English) or ``Nenhum problema encontrado.`` (Portuguese).
193
135
 
194
136
  Args:
195
137
  verbosity: Review depth key — one of ``"quick"``, ``"detailed"``,
196
138
  or ``"security"``. Any unrecognised value falls back to
197
139
  ``"detailed"``.
198
- language: Response language code — ``"en"`` for English,
199
- ``"pt"`` for Portuguese.
200
140
 
201
141
  Returns:
202
- The system prompt string for the resolved verbosity and language.
142
+ The system prompt string for the resolved verbosity.
203
143
  """
204
- prompts = SYSTEM_PROMPTS.get(verbosity, SYSTEM_PROMPTS["detailed"])
205
- return prompts.get(language, prompts["pt"])
206
-
207
-
208
- def get_pr_comment_prompt(language: str) -> str:
209
- """Returns the prompt that requests structured JSON PR comments from the LLM.
144
+ return SYSTEM_PROMPTS.get(verbosity, SYSTEM_PROMPTS["detailed"])
210
145
 
211
- Retrieves the prompt from ``PR_COMMENT_PROMPT`` for the given *language*.
212
- If the language key is not present, ``"pt"`` (Portuguese) is used as the
213
- default.
214
146
 
215
- The returned prompt instructs the LLM to produce a single valid JSON
216
- object with a ``"summary"`` field (narrative review text) and a
217
- ``"comments"`` field (array of review comment objects, each with the
218
- fields ``file``, ``line``, ``type``, ``comment``, ``suggestion``,
219
- and ``reference``).
220
-
221
- Args:
222
- language: Response language code — ``"en"`` for English,
223
- ``"pt"`` for Portuguese.
224
-
225
- Returns:
226
- The prompt string used to request structured PR comments.
227
- """
228
- return PR_COMMENT_PROMPT.get(language, PR_COMMENT_PROMPT["pt"])
229
-
230
-
231
- def get_scope_guidance(review_scope: str, language: str, structured: bool = False) -> str:
147
+ def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
232
148
  """Returns LLM instructions tailored to the active review scope.
233
149
 
234
150
  For ``diff_only`` scope, the instructions inform the LLM that:
@@ -248,8 +164,6 @@ def get_scope_guidance(review_scope: str, language: str, structured: bool = Fals
248
164
 
249
165
  Args:
250
166
  review_scope: ``"diff_only"`` (default) or ``"full_code"``.
251
- language: Response language code — ``"en"`` for English,
252
- ``"pt"`` for Portuguese.
253
167
  structured: When ``True``, appends additional constraints for the
254
168
  structured JSON comment mode (every comment must carry a valid
255
169
  file path and line number > 0 for inline posting).
@@ -260,52 +174,29 @@ def get_scope_guidance(review_scope: str, language: str, structured: bool = Fals
260
174
  scope = (review_scope or "diff_only").lower()
261
175
 
262
176
  if scope == "full_code":
263
- if language == "en":
264
- return (
265
- "Review scope: full_code. The diff contains only added lines (+) for each file. "
266
- "Analyze the complete content of the changed files and identify issues in the new code. "
267
- "Do not comment on deleted or absent code."
268
- )
269
177
  return (
270
- "Ambito de review: full_code. O diff contém apenas linhas adicionadas (+) de cada ficheiro. "
271
- "Analisa o conteúdo completo dos ficheiros alterados e identifica problemas no novo código. "
272
- "Não comentes código eliminado ou ausente."
178
+ "Review scope: full_code. The diff contains only added lines (+) for each file. "
179
+ "Analyze the complete content of the changed files and identify issues in the new code. "
180
+ "Do not comment on deleted or absent code."
273
181
  )
274
182
 
275
183
  if structured:
276
- if language == "en":
277
- return (
278
- "Review scope: diff_only. The diff contains only added lines (+) — context and deletions were removed. "
279
- "The complete new-version file content, after the changes (between ### FULL_FILE_CONTEXT_START and ### FULL_FILE_CONTEXT_END markers) "
280
- "is provided for each file as read-only context. "
281
- "Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines (marked + in the diff). "
282
- "Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes. "
283
- "For every problem, you MUST provide a valid file and line (>0) to allow inline comments. "
284
- "Do not emit general problem comments without file/line."
285
- )
286
- return (
287
- "Ambito de review: diff_only. O diff contém apenas linhas adicionadas (+) — contexto e eliminações foram removidos. "
288
- "É fornecido o conteúdo completo do ficheiro na versão nova, depois das alterações (entre os marcadores ### FULL_FILE_CONTEXT_START e ### FULL_FILE_CONTEXT_END) como contexto de leitura. "
289
- "Usa-a para compreender o código envolvente, mas foca o teu review EXCLUSIVAMENTE nas linhas alteradas (marcadas com + no diff). "
290
- "NÃO reportes problemas em linhas não alteradas, exceto se afetarem diretamente a correção das alterações. "
291
- "Para cada problema, DEVE ser fornecido file e line válidos (>0) para comentário inline. "
292
- "Não emitas comentários gerais de problema sem file/line."
293
- )
294
-
295
- if language == "en":
296
184
  return (
297
- "Review scope: diff_only. The diff contains only added lines (+). "
298
- "The new-version (post-change) full file content (between ### FULL_FILE_CONTEXT_START and ### FULL_FILE_CONTEXT_END markers) "
185
+ "Review scope: diff_only. The diff contains only added lines (+) — context and deletions were removed. "
186
+ "The complete new-version file content, after the changes (between ### FULL_FILE_CONTEXT_START and ### FULL_FILE_CONTEXT_END markers) "
299
187
  "is provided for each file as read-only context. "
300
- "Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines. "
301
- "Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes."
188
+ "Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines (marked + in the diff). "
189
+ "Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes. "
190
+ "For every problem, you MUST provide a valid file and line (>0) to allow inline comments. "
191
+ "Do not emit general problem comments without file/line."
302
192
  )
193
+
303
194
  return (
304
- "Ambito de review: diff_only. O diff contém apenas linhas adicionadas (+). "
305
- "Uma secção com o conteúdo completo do ficheiro na versão nova, após as alterações (entre os marcadores ### FULL_FILE_CONTEXT_START e ### FULL_FILE_CONTEXT_END) "
306
- "é fornecida como contexto de leitura. "
307
- "Usa-a para compreender o código envolvente, mas foca o teu review EXCLUSIVAMENTE nas linhas alteradas. "
308
- "NÃO reportes problemas em linhas não alteradas, exceto se afetarem diretamente a correção das alterações."
195
+ "Review scope: diff_only. The diff contains only added lines (+). "
196
+ "The new-version (post-change) full file content (between ### FULL_FILE_CONTEXT_START and ### FULL_FILE_CONTEXT_END markers) "
197
+ "is provided for each file as read-only context. "
198
+ "Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines. "
199
+ "Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes."
309
200
  )
310
201
 
311
202
 
@@ -407,13 +298,18 @@ class LLMClient:
407
298
  is a list of dicts with keys: file, line, type, comment, suggestion,
408
299
  reference.
409
300
  """
410
- base_prompt = get_system_prompt(self.config.verbosity, self.config.review_language)
411
- json_schema_prompt = get_pr_comment_prompt(self.config.review_language)
301
+ base_prompt = get_system_prompt(self.config.verbosity)
302
+ json_schema_prompt = PR_COMMENT_PROMPT
412
303
  custom_prompt = self._load_custom_prompt_text()
413
304
 
305
+ # filter custom_prompt sections by the languages/extensions actually changed
306
+ if custom_prompt:
307
+ file_paths = [f.get("file") for f in files_summary]
308
+ active_langs = detect_langs(file_paths)
309
+ custom_prompt = filter_prompt_by_langs(custom_prompt, active_langs) # can return an empty list
310
+
414
311
  scope_guidance = get_scope_guidance(
415
312
  review_scope=review_scope,
416
- language=self.config.review_language,
417
313
  structured=True,
418
314
  )
419
315
 
@@ -0,0 +1,56 @@
1
+ """
2
+ Prompt Utilities Module - AI Code Review
3
+ ========================================
4
+ Responsible for tailoring the custom review prompt (review_prompt.md) to the
5
+ files actually changed in a diff:
6
+ - Detects which "languages" (file extensions) are present in the changeset
7
+ - Filters the custom prompt content, keeping only the sections tagged for
8
+ those extensions (plus sections tagged "all", which always apply)
9
+
10
+ No manual extension-to-language mapping is required: the file extension
11
+ itself (without the dot, lowercase) is used as the tag. This means the
12
+ prompt file can be extended with new languages (e.g. Java, Go) without any
13
+ code changes - just add a new <!-- lang: ... --> section to review_prompt.md.
14
+ """
15
+
16
+ import os
17
+ import re
18
+
19
+ LANG_TAG_RE = re.compile(r"<!--\s*lang:\s*(.*?)\s*-->")
20
+
21
+
22
+ def detect_langs(file_paths: list[str]) -> set[str]:
23
+ """
24
+ Detects active tags directly from the extensions of changed files.
25
+ 'all' is always included since general sections always apply.
26
+ No manual mapping: the extension (no dot, lowercase) IS the tag.
27
+ """
28
+ langs = {"all"}
29
+ for path in file_paths:
30
+ if not path:
31
+ continue
32
+ ext = os.path.splitext(path)[1].lstrip(".").lower()
33
+ if ext:
34
+ langs.add(ext)
35
+ return langs
36
+
37
+
38
+ def filter_prompt_by_langs(md_content: str, active_langs: set[str]) -> str:
39
+ """
40
+ Parses review_prompt.md and returns only the sections whose
41
+ <!-- lang: ... --> tag intersects with active_langs.
42
+ """
43
+ active_langs = {l.strip().lower() for l in active_langs}
44
+ blocks = re.split(r"(<!--\s*lang:.*?-->)", md_content)
45
+
46
+ output: list[str] = []
47
+ current_langs = {"all"}
48
+ for chunk in blocks:
49
+ match = LANG_TAG_RE.match(chunk.strip())
50
+ if match:
51
+ current_langs = {l.strip().lower() for l in match.group(1).split(",")}
52
+ continue
53
+ if current_langs & active_langs:
54
+ output.append(chunk)
55
+
56
+ return "\n".join(output).strip()
@@ -72,9 +72,6 @@ tfs:
72
72
  # Review Preferences
73
73
  # ---------------------------------------------------------------------------
74
74
  review:
75
- # Language: "pt" or "en"
76
- language: en
77
-
78
75
  # Default verbosity: "quick", "detailed" or "security"
79
76
  verbosity: detailed
80
77
 
@@ -0,0 +1,91 @@
1
+ <!-- lang: all -->
2
+ ## General Rules (all languages)
3
+ - Security: SQL injection, XSS, hardcoded secrets/credentials, missing input validation, sensitive data exposed in logs.
4
+ - Error handling: swallowed exceptions (empty catch), missing logging at critical points, generic error messages to end users.
5
+ - Readability: methods too long or with too many responsibilities (SRP), poor naming, dead/commented-out code.
6
+ - Duplication: repeated logic that should be extracted/reused.
7
+ - Testability: hard-to-mock dependencies, business logic mixed with I/O.
8
+ - Unused code: unused local variables, method parameters, private fields/methods, unreachable code, and unused imports/usings.
9
+ - Unnecessary allocations in hot paths (loops, frequently-called requests).
10
+
11
+ <!-- lang: cs,ts -->
12
+ ## Performance
13
+ - Use of `StringBuilder` vs string concatenation in loops.
14
+ - Blocking synchronous operations where async should be used (I/O, network calls).
15
+
16
+ <!-- lang: cs,ts,sql -->
17
+ ## Concurrency / Thread-safety
18
+ - Race conditions in shared code (static fields, singletons).
19
+ - Correct use of locks/semaphores, avoid deadlocks.
20
+ - Thread-safe collections where needed (`ConcurrentDictionary`, etc).
21
+
22
+ <!-- lang: all -->
23
+ ## Configuration & Secrets
24
+ - Hardcoded values that should be in configuration (connection strings, URLs, timeouts).
25
+ - Secrets/API keys accidentally committed.
26
+
27
+ <!-- lang: cs,ts -->
28
+ ## Dependencies & Compatibility
29
+ - New dependencies added without clear justification - unnecessary or outdated packages.
30
+ - Breaking changes when updating package versions.
31
+
32
+ <!-- lang: cs -->
33
+ ## C#
34
+ - Naming Conventions & Casing: follow Microsoft's official C# naming guidelines (https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names).
35
+ - XML docs: follow Microsoft's official documentation guidance (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/).
36
+ - Async: avoid `async void` (except handlers), use `ConfigureAwait(false)` in libraries, don't block with `.Result`/`.Wait()`.
37
+ - Nullable refs: correct `?` usage, avoid unjustified `!`.
38
+ - LINQ: avoid redundant iterations, N+1 queries with EF Core.
39
+ - Disposal: proper use of `IDisposable`/`using` for resources (streams, connections, etc).
40
+ - Immutability: prefer `readonly`, records, immutable types.
41
+
42
+ <!-- lang: cs,sql -->
43
+ ## Logging & Observability
44
+ - Appropriate log levels (Debug vs Info vs Warning vs Error).
45
+ - Don't log sensitive data (PII, passwords, tokens).
46
+
47
+ <!-- lang: sql -->
48
+ ## Migrations / Database (given your SQL/EF stack)
49
+ - Destructive changes to columns/tables with existing data (data loss risk).
50
+ - Migrations that lock large tables in production.
51
+
52
+ <!-- lang: sql -->
53
+ ## SQL
54
+ - Avoid `SELECT *`; specify columns explicitly.
55
+ - Check for parameterized queries (avoid string concatenation → SQL injection).
56
+ - Indexes: flag queries likely to cause table scans on large tables.
57
+ - Transactions: ensure multi-step critical operations are wrapped in a transaction.
58
+ - Naming conventions for tables/columns consistent with the rest of the schema.
59
+
60
+ <!-- lang: xml -->
61
+ ## XML
62
+ - Validation against schema (XSD) where applicable.
63
+ - Consistent formatting/indentation.
64
+ - Avoid duplicated configuration that could be centralized.
65
+
66
+ <!-- lang: html -->
67
+ ## HTML
68
+ - A11y: `alt`, `aria-*`, labels on inputs, semantic tags (`<button>` vs `<div onclick>`).
69
+ - Semantic tags where they apply (`<button>`, `<label>`, `<ul>`/`<li>`) over generic `<div>`/`<span>`. Landmarks (`<header>`/`<nav>`/`<main>`) apply to shell/layout, not feature components.
70
+ - No inline `style=""` for static styling - use the component's `.less` file. `[style.x]`/`[ngStyle]` ok for dynamic values.
71
+
72
+ <!-- lang: ts,html -->
73
+ ## TypeScript / Angular
74
+ - Follow the Angular style guide (https://angular.dev/style-guide).
75
+ - Avoid `any`, prefer explicit types.
76
+ - RxJS: unsubscribe via async pipe, `takeUntil`, or `takeUntilDestroyed`.
77
+ - Use `OnPush` where it makes sense.
78
+ - Separate presentation (component) from business logic (services); avoid heavy template logic.
79
+ - Files: kebab-case + type suffix (`select-maintenance-plan-step.component.ts/.html/.less`).
80
+ - Classes: PascalCase matching file name, no type suffix (`SelectMaintenancePlanStep`, not `...Component`).
81
+ - Selectors: kebab-case with project prefix (`app-select-maintenance-plan-step`).
82
+ - `.less` files named like their component; use `:host`, avoid global style leaks.
83
+
84
+ <!-- lang: all -->
85
+ ## Review Process Notes (meta)
86
+ - Distinguish blocking comments (must-fix) from suggestions (nice-to-have) - this matters a lot for an automated reviewer, so it doesn't hold up merges over trivial issues.
87
+ - If the change is purely formatting/whitespace, do not comment on it.
88
+ - Ignore auto-generated files (generated migrations, `*.designer.cs`, `node_modules`, etc.) unless they contain manually-added code.
89
+ - Flag only issues relevant to the diff/change being reviewed, not the entire file, unless the change requires it.
90
+ - Before flagging something as "missing", verify it isn't present in the surrounding unchanged context (lines without `+`/`-`). Only flag genuinely absent elements, not elements outside the shown diff hunk.
91
+ - Priority order: Security > Functional bugs > Maintainability > Style.
@@ -17,6 +17,14 @@ def test_get_effective_model_prefers_explicit_value() -> None:
17
17
  assert config.get_effective_model() == "custom-model"
18
18
 
19
19
 
20
+ def test_review_config_has_no_review_language_attribute() -> None:
21
+ """Regression: the configurable review language feature was removed."""
22
+ config = ReviewConfig()
23
+
24
+ assert hasattr(config, "review_language") is False
25
+ assert "review_language" not in ReviewConfig.__dataclass_fields__
26
+
27
+
20
28
  @pytest.mark.parametrize(
21
29
  ("provider", "field", "expected"),
22
30
  [
@@ -171,7 +179,6 @@ tfs:
171
179
  ca_bundle: ~/ca.pem
172
180
  repository: repo-a
173
181
  review:
174
- language: en
175
182
  verbosity: quick
176
183
  scope: full_code
177
184
  max_diff_files: 12
@@ -215,7 +222,6 @@ output:
215
222
  assert config.tfs_verify_ssl is False
216
223
  assert config.tfs_ca_bundle == "~/ca.pem"
217
224
  assert config.tfs_repository == "repo-a"
218
- assert config.review_language == "en"
219
225
  assert config.verbosity == "quick"
220
226
  assert config.review_scope == "full_code"
221
227
  assert config.max_diff_files == 12
@@ -10,7 +10,8 @@ from types import ModuleType, SimpleNamespace
10
10
  import pytest
11
11
 
12
12
  from src.config import ReviewConfig
13
- from src.llm_client import LLMClient, LLMError, build_user_message, get_pr_comment_prompt, get_scope_guidance, get_system_prompt
13
+ from src import llm_client
14
+ from src.llm_client import LLMClient, LLMError, build_user_message, get_scope_guidance, get_system_prompt, PR_COMMENT_PROMPT
14
15
 
15
16
 
16
17
  class FakeResponse:
@@ -79,7 +80,6 @@ def make_llm_config(**changes: object) -> ReviewConfig:
79
80
  model="gpt-4o-mini",
80
81
  max_tokens=256,
81
82
  temperature=0.2,
82
- review_language="en",
83
83
  verbosity="detailed",
84
84
  )
85
85
  for key, value in changes.items():
@@ -89,88 +89,72 @@ def make_llm_config(**changes: object) -> ReviewConfig:
89
89
 
90
90
  def test_prompt_helpers_select_expected_language_and_scope() -> None:
91
91
  """It should select prompts and scope guidance consistently."""
92
- assert "code reviewer" in get_system_prompt("quick", "en").lower()
93
- assert "JSON" in get_pr_comment_prompt("pt")
94
- assert "full_code" in get_scope_guidance("full_code", "en")
95
- assert "file e line" in get_scope_guidance("diff_only", "pt", structured=True)
96
- assert "added lines" in get_scope_guidance("diff_only", "en")
92
+ assert "code reviewer" in get_system_prompt("quick").lower()
93
+ assert "JSON" in PR_COMMENT_PROMPT
94
+ assert "full_code" in get_scope_guidance("full_code")
95
+ assert "file and line" in get_scope_guidance("diff_only", structured=True).lower()
96
+ assert "added lines" in get_scope_guidance("diff_only")
97
+
98
+
99
+ def test_get_pr_comment_prompt_function_removed() -> None:
100
+ """Regression: get_pr_comment_prompt() was replaced by PR_COMMENT_PROMPT."""
101
+ assert not hasattr(llm_client, "get_pr_comment_prompt")
102
+
103
+
104
+ def test_get_system_prompt_and_get_scope_guidance_reject_language_kwarg() -> None:
105
+ """Regression: language parameter was removed from both prompt helpers."""
106
+ with pytest.raises(TypeError):
107
+ get_system_prompt("quick", language="pt") # type: ignore[call-arg]
108
+
109
+ with pytest.raises(TypeError):
110
+ get_scope_guidance("diff_only", structured=False, language="pt") # type: ignore[call-arg]
111
+
97
112
 
98
113
 
99
114
  # ---------------------------------------------------------------------------
100
115
  # SYSTEM_PROMPTS / get_system_prompt — Phase 1 change validation
101
116
  # ---------------------------------------------------------------------------
102
117
 
103
- @pytest.mark.parametrize("language", ["en", "pt"])
104
- def test_get_system_prompt_quick_returns_non_empty_string(language: str) -> None:
105
- """quick prompts must return a non-empty string for every supported language."""
106
- prompt = get_system_prompt("quick", language)
118
+ def test_get_system_prompt_quick_returns_non_empty_string() -> None:
119
+ """quick prompt must return a non-empty string."""
120
+ prompt = get_system_prompt("quick")
107
121
  assert isinstance(prompt, str)
108
122
  assert len(prompt.strip()) > 0
109
123
 
110
124
 
111
- def test_get_system_prompt_detailed_en_enforces_inline_comment_format() -> None:
112
- """detailed/en must specify the inline comment output format after Phase 1."""
113
- prompt = get_system_prompt("detailed", "en")
125
+ def test_get_system_prompt_detailed_enforces_inline_comment_format() -> None:
126
+ """detailed must specify the inline comment output format after Phase 1."""
127
+ prompt = get_system_prompt("detailed")
114
128
  # Must instruct the LLM to return only inline comments
115
129
  assert "inline comments" in prompt.lower()
116
130
  # Must include the exact per-line format marker
117
131
  assert "Line <line_number>" in prompt
118
132
 
119
133
 
120
- def test_get_system_prompt_detailed_en_forbids_summary_sections() -> None:
121
- """detailed/en must explicitly forbid summary sections after Phase 1."""
122
- prompt = get_system_prompt("detailed", "en")
134
+ def test_get_system_prompt_detailed_forbids_summary_sections() -> None:
135
+ """detailed must explicitly forbid summary sections after Phase 1."""
136
+ prompt = get_system_prompt("detailed")
123
137
  assert "Do NOT produce summary sections" in prompt
124
138
 
125
139
 
126
- def test_get_system_prompt_detailed_en_contains_no_focus_on_or_structured_review_language() -> None:
127
- """detailed/en must NOT contain generic 'Focus on' or 'structured review' language after Phase 1."""
128
- prompt = get_system_prompt("detailed", "en")
129
- assert "focus on" not in prompt.lower()
130
- assert "structured review" not in prompt.lower()
131
-
132
-
133
- def test_get_system_prompt_detailed_pt_enforces_inline_comment_format() -> None:
134
- """detailed/pt must specify the inline comment output format in Portuguese after Phase 1."""
135
- prompt = get_system_prompt("detailed", "pt")
136
- assert "inline" in prompt.lower()
137
- # Must include the per-line format marker in Portuguese
138
- assert "Linha" in prompt
139
-
140
-
141
- def test_get_system_prompt_detailed_pt_forbids_summary_sections() -> None:
142
- """detailed/pt must explicitly forbid summary sections in Portuguese after Phase 1."""
143
- prompt = get_system_prompt("detailed", "pt")
144
- assert "NÃO produzas" in prompt
145
- # The forbidden construct must mention summary sections
146
- assert "sumário" in prompt.lower() or "secções" in prompt.lower()
147
-
148
-
149
- def test_get_system_prompt_detailed_pt_contains_no_focus_on_or_structured_review_language() -> None:
150
- """detailed/pt must NOT contain generic 'Focus on' or 'structured review' language after Phase 1."""
151
- prompt = get_system_prompt("detailed", "pt")
140
+ def test_get_system_prompt_detailed_contains_no_focus_on_or_structured_review_language() -> None:
141
+ """detailed must NOT contain generic 'Focus on' or 'structured review' language after Phase 1."""
142
+ prompt = get_system_prompt("detailed")
152
143
  assert "focus on" not in prompt.lower()
153
144
  assert "structured review" not in prompt.lower()
154
145
 
155
146
 
156
- def test_get_system_prompt_security_en_mentions_security_specific_terms() -> None:
157
- """security/en must include core vulnerability types it is expected to check for."""
158
- prompt = get_system_prompt("security", "en")
147
+ def test_get_system_prompt_security_mentions_security_specific_terms() -> None:
148
+ """security must include core vulnerability types it is expected to check for."""
149
+ prompt = get_system_prompt("security")
159
150
  for term in ("SQL Injection", "XSS", "CSRF"):
160
151
  assert term in prompt, f"Security prompt missing expected term: '{term}'"
161
152
 
162
153
 
163
154
  def test_get_system_prompt_unknown_verbosity_falls_back_to_detailed() -> None:
164
155
  """An unrecognised verbosity level must silently fall back to the 'detailed' prompt."""
165
- fallback = get_system_prompt("unknown_verbosity", "en")
166
- expected = get_system_prompt("detailed", "en")
167
- assert fallback == expected
168
-
169
-
170
- def test_get_system_prompt_unknown_language_falls_back_to_pt() -> None:
171
- """An unrecognised language code must fall back to the Portuguese ('pt') prompt."""
172
- fallback = get_system_prompt("detailed", "xx")
173
- expected = get_system_prompt("detailed", "pt")
156
+ fallback = get_system_prompt("unknown_verbosity")
157
+ expected = get_system_prompt("detailed")
174
158
  assert fallback == expected
175
159
 
176
160
 
@@ -179,39 +163,35 @@ def test_get_system_prompt_unknown_language_falls_back_to_pt() -> None:
179
163
  # ---------------------------------------------------------------------------
180
164
 
181
165
  def test_get_scope_guidance_diff_only_references_full_file_context_markers() -> None:
182
- """diff_only guidance must reference FULL_FILE_CONTEXT_START and _END for all language/mode combos."""
183
- for lang in ("en", "pt"):
184
- for structured in (False, True):
185
- guidance = get_scope_guidance("diff_only", lang, structured=structured)
186
- assert "FULL_FILE_CONTEXT_START" in guidance, f"lang={lang} structured={structured}"
187
- assert "FULL_FILE_CONTEXT_END" in guidance, f"lang={lang} structured={structured}"
166
+ """diff_only guidance must reference FULL_FILE_CONTEXT_START and _END for both modes."""
167
+ for structured in (False, True):
168
+ guidance = get_scope_guidance("diff_only", structured=structured)
169
+ assert "FULL_FILE_CONTEXT_START" in guidance, f"structured={structured}"
170
+ assert "FULL_FILE_CONTEXT_END" in guidance, f"structured={structured}"
188
171
 
189
172
 
190
- @pytest.mark.parametrize("language", ["en", "pt"])
191
- def test_get_scope_guidance_diff_only_structured_demands_valid_line_number(language: str) -> None:
173
+ def test_get_scope_guidance_diff_only_structured_demands_valid_line_number() -> None:
192
174
  """Structured diff_only guidance must instruct the LLM to supply a line number > 0."""
193
- guidance = get_scope_guidance("diff_only", language, structured=True)
175
+ guidance = get_scope_guidance("diff_only", structured=True)
194
176
  assert "(>0)" in guidance
195
177
 
196
178
 
197
179
  def test_get_scope_guidance_diff_only_non_structured_focuses_on_changed_lines() -> None:
198
180
  """Non-structured diff_only guidance must tell the LLM to focus on changed/added lines."""
199
- guidance_en = get_scope_guidance("diff_only", "en", structured=False)
200
- guidance_pt = get_scope_guidance("diff_only", "pt", structured=False)
181
+ guidance_en = get_scope_guidance("diff_only", structured=False)
201
182
 
202
183
  assert "changed lines" in guidance_en.lower() or "added lines" in guidance_en.lower()
203
- assert "alteradas" in guidance_pt.lower() or "adicionadas" in guidance_pt.lower()
204
184
 
205
185
 
206
186
  def test_get_scope_guidance_diff_only_structured_does_not_demand_line_in_non_structured() -> None:
207
187
  """Non-structured diff_only guidance must NOT mandate a minimum line number."""
208
- guidance_en = get_scope_guidance("diff_only", "en", structured=False)
188
+ guidance_en = get_scope_guidance("diff_only", structured=False)
209
189
  assert "(>0)" not in guidance_en
210
190
 
211
191
 
212
192
  def test_get_scope_guidance_unknown_scope_falls_back_to_diff_only() -> None:
213
193
  """An unrecognised review_scope must fall through and return diff_only guidance."""
214
- guidance = get_scope_guidance("unknown_scope", "en")
194
+ guidance = get_scope_guidance("unknown_scope")
215
195
 
216
196
  assert "diff_only" in guidance
217
197
  assert "FULL_FILE_CONTEXT_START" in guidance
@@ -267,6 +247,120 @@ def test_review_pr_dispatches_and_merges_custom_prompt(mocker, tmp_path: Path) -
267
247
  assert "Custom user instructions" in system_prompt
268
248
  assert "Custom context loaded from" in user_message
269
249
 
250
+ def test_review_omits_custom_instructions_header_when_filter_empties_prompt(mocker, tmp_path: Path) -> None:
251
+ """If filtering removes all sections, the 'Custom user instructions' header should not appear."""
252
+ prompt_file = tmp_path / "prompt.md"
253
+ prompt_file.write_text(
254
+ "<!-- lang: java -->\n"
255
+ "## Java Rules\n"
256
+ "- Use streams.\n",
257
+ encoding="utf-8",
258
+ )
259
+ config = make_llm_config(custom_prompt_file=str(prompt_file))
260
+ client = LLMClient(config)
261
+ openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
262
+
263
+ client.review("+code", [{"file": "component.ts", "additions": 1, "deletions": 0}])
264
+
265
+ system_prompt, user_message = openai.call_args.args[:2]
266
+ assert "Custom user instructions" not in system_prompt
267
+ assert "Custom context loaded from" not in user_message
268
+
269
+ def test_review_filters_custom_prompt_sections_by_changed_extensions(mocker, tmp_path: Path) -> None:
270
+ """Only sections matching the extensions of changed files (plus 'all') should be kept."""
271
+ prompt_file = tmp_path / "prompt.md"
272
+ prompt_file.write_text(
273
+ "<!-- lang: all -->\n"
274
+ "## General\n"
275
+ "- Always check for secrets.\n"
276
+ "\n"
277
+ "<!-- lang: cs -->\n"
278
+ "## CSharp Rules\n"
279
+ "- Avoid magic strings.\n"
280
+ "\n"
281
+ "<!-- lang: py -->\n"
282
+ "## Python Rules\n"
283
+ "- Use type hints.\n",
284
+ encoding="utf-8",
285
+ )
286
+ config = make_llm_config(custom_prompt_file=str(prompt_file))
287
+ client = LLMClient(config)
288
+ openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
289
+
290
+ client.review("+code", [{"file": "app.py", "additions": 1, "deletions": 0}])
291
+
292
+ system_prompt, _ = openai.call_args.args[:2]
293
+ assert "General" in system_prompt
294
+ assert "Python Rules" in system_prompt
295
+ assert "CSharp Rules" not in system_prompt
296
+
297
+
298
+ def test_review_keeps_only_all_sections_when_no_matching_extension(mocker, tmp_path: Path) -> None:
299
+ """If no changed file matches a tagged section, only 'all' sections remain."""
300
+ prompt_file = tmp_path / "prompt.md"
301
+ prompt_file.write_text(
302
+ "<!-- lang: all -->\n"
303
+ "## General\n"
304
+ "- Always check for secrets.\n"
305
+ "\n"
306
+ "<!-- lang: java -->\n"
307
+ "## Java Rules\n"
308
+ "- Use streams.\n",
309
+ encoding="utf-8",
310
+ )
311
+ config = make_llm_config(custom_prompt_file=str(prompt_file))
312
+ client = LLMClient(config)
313
+ openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
314
+
315
+ client.review("+code", [{"file": "component.ts", "additions": 1, "deletions": 0}])
316
+
317
+ system_prompt, _ = openai.call_args.args[:2]
318
+ assert "General" in system_prompt
319
+ assert "Java Rules" not in system_prompt
320
+
321
+
322
+ def test_review_includes_multiple_extension_sections(mocker, tmp_path: Path) -> None:
323
+ """Changed files with different extensions should each pull in their matching section."""
324
+ prompt_file = tmp_path / "prompt.md"
325
+ prompt_file.write_text(
326
+ "<!-- lang: ts -->\n"
327
+ "## TypeScript Rules\n"
328
+ "- No any types.\n"
329
+ "\n"
330
+ "<!-- lang: html -->\n"
331
+ "## Html Rules\n"
332
+ "- No inline styles.\n",
333
+ encoding="utf-8",
334
+ )
335
+ config = make_llm_config(custom_prompt_file=str(prompt_file))
336
+ client = LLMClient(config)
337
+ openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
338
+
339
+ client.review(
340
+ "+code",
341
+ [
342
+ {"file": "foo.component.ts", "additions": 1, "deletions": 0},
343
+ {"file": "foo.component.html", "additions": 1, "deletions": 0},
344
+ ],
345
+ )
346
+
347
+ system_prompt, _ = openai.call_args.args[:2]
348
+ assert "TypeScript Rules" in system_prompt
349
+ assert "Html Rules" in system_prompt
350
+
351
+
352
+ def test_review_without_lang_tags_keeps_full_custom_prompt(mocker, tmp_path: Path) -> None:
353
+ """Custom prompt with no <!-- lang: --> tags at all should pass through unchanged."""
354
+ prompt_file = tmp_path / "prompt.md"
355
+ prompt_file.write_text("Always mention tests", encoding="utf-8")
356
+ config = make_llm_config(custom_prompt_file=str(prompt_file))
357
+ client = LLMClient(config)
358
+ openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
359
+
360
+ client.review("+code", [{"file": "a.py", "additions": 1, "deletions": 0}])
361
+
362
+ system_prompt, _ = openai.call_args.args[:2]
363
+ assert "Always mention tests" in system_prompt
270
364
 
271
365
  def test_review_pr_raises_for_unsupported_provider() -> None:
272
366
  """It should reject unsupported providers before any HTTP call."""
@@ -0,0 +1,129 @@
1
+ """
2
+ Tests for prompt_utils.py - language/extension based filtering of the custom review prompt.
3
+ """
4
+ from src.prompt_utils import detect_langs, filter_prompt_by_langs
5
+
6
+ class TestDetectLangs:
7
+
8
+ def test_always_includes_all(self) -> None:
9
+ """'all' must always be present, even with no files."""
10
+ assert detect_langs([]) == {"all"}
11
+
12
+ def test_detects_single_extension(self) -> None:
13
+ result = detect_langs(["src/foo.py"])
14
+ assert result == {"all", "py"}
15
+
16
+ def test_detects_multiple_distinct_extensions(self) -> None:
17
+ result = detect_langs(["a.ts", "b.html", "c.cs"])
18
+ assert result == {"all", "ts", "html", "cs"}
19
+
20
+ def test_deduplicates_same_extension(self) -> None:
21
+ result = detect_langs(["a.py", "b.py", "c.py"])
22
+ assert result == {"all", "py"}
23
+
24
+ def test_extension_lowercased(self) -> None:
25
+ result = detect_langs(["Component.TS"])
26
+ assert result == {"all", "ts"}
27
+
28
+ def test_ignores_none_or_empty_paths(self) -> None:
29
+ result = detect_langs([None, "", "a.py"])
30
+ assert result == {"all", "py"}
31
+
32
+ def test_file_without_extension_is_ignored(self) -> None:
33
+ result = detect_langs(["Dockerfile", "Makefile"])
34
+ assert result == {"all"}
35
+
36
+ def test_nested_path_extracts_extension_correctly(self) -> None:
37
+ result = detect_langs(["src/app/components/foo.component.ts"])
38
+ assert result == {"all", "ts"}
39
+
40
+ def test_dotfile_with_no_further_extension_is_ignored(self) -> None:
41
+ # os.path.splitext(".gitignore") -> ('.gitignore', '') -> no extension detected
42
+ result = detect_langs([".gitignore"])
43
+ assert result == {"all"}
44
+
45
+
46
+ class TestFilterPromptByLangs:
47
+
48
+ def test_content_without_any_tag_is_kept_entirely(self) -> None:
49
+ content = "Always mention tests"
50
+ result = filter_prompt_by_langs(content, {"all"})
51
+ assert result == "Always mention tests"
52
+
53
+ def test_all_section_is_always_included(self) -> None:
54
+ content = (
55
+ "<!-- lang: all -->\n"
56
+ "## General\n"
57
+ "- Rule A\n"
58
+ )
59
+ result = filter_prompt_by_langs(content, {"all", "cs"})
60
+ assert "## General" in result
61
+ assert "Rule A" in result
62
+
63
+ def test_matching_lang_section_is_included(self) -> None:
64
+ content = (
65
+ "<!-- lang: cs,ts -->\n"
66
+ "## Dependencies\n"
67
+ "- Rule B\n"
68
+ )
69
+ result = filter_prompt_by_langs(content, {"all", "ts"})
70
+ assert "## Dependencies" in result
71
+ assert "Rule B" in result
72
+
73
+ def test_non_matching_lang_section_is_excluded(self) -> None:
74
+ content = (
75
+ "<!-- lang: java -->\n"
76
+ "## Java Rules\n"
77
+ "- Rule C\n"
78
+ )
79
+ result = filter_prompt_by_langs(content, {"all", "ts"})
80
+ assert "Java Rules" not in result
81
+ assert "Rule C" not in result
82
+
83
+ def test_mixed_sections_filters_correctly(self) -> None:
84
+ content = (
85
+ "<!-- lang: all -->\n"
86
+ "## General\n"
87
+ "- General rule\n"
88
+ "\n"
89
+ "<!-- lang: cs -->\n"
90
+ "## CSharp\n"
91
+ "- CSharp rule\n"
92
+ "\n"
93
+ "<!-- lang: html -->\n"
94
+ "## Html\n"
95
+ "- Html rule\n"
96
+ )
97
+ result = filter_prompt_by_langs(content, {"all", "html"})
98
+
99
+ assert "General rule" in result
100
+ assert "Html rule" in result
101
+ assert "CSharp rule" not in result
102
+ assert "CSharp" not in result
103
+
104
+ def test_tags_are_case_insensitive(self) -> None:
105
+ content = (
106
+ "<!-- lang: CS,TS -->\n"
107
+ "## Dependencies\n"
108
+ "- Rule\n"
109
+ )
110
+ result = filter_prompt_by_langs(content, {"all", "cs"})
111
+ assert "Rule" in result
112
+
113
+ def test_no_matching_section_returns_empty(self) -> None:
114
+ content = (
115
+ "<!-- lang: java -->\n"
116
+ "## Java Rules\n"
117
+ "- Rule\n"
118
+ )
119
+ result = filter_prompt_by_langs(content, {"all", "ts"})
120
+ assert result == ""
121
+
122
+ def test_tag_with_extra_whitespace_is_parsed(self) -> None:
123
+ content = (
124
+ "<!-- lang: cs , ts -->\n"
125
+ "## Dependencies\n"
126
+ "- Rule\n"
127
+ )
128
+ result = filter_prompt_by_langs(content, {"all", "ts"})
129
+ assert "Rule" in result
@@ -1,2 +0,0 @@
1
- - Naming Conventions & Casing, follow Microsoft's official C# naming guidelines (https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names).
2
- - XML Documentation Comments, follow Microsoft's official code documentation (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/).