pr-context-engine 0.1.3__tar.gz → 0.1.4__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 (78) hide show
  1. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/CHANGELOG.md +12 -0
  2. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/PKG-INFO +1 -1
  3. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/pyproject.toml +1 -1
  4. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/analyzers/risk_scorer.py +47 -1
  5. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/briefing/prompt_templates.py +6 -0
  6. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_risk_scorer.py +103 -0
  7. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.claude/commands/publish-pypi.md +0 -0
  8. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.env.example +0 -0
  9. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.github/pull_request_template.md +0 -0
  12. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.github/workflows/pr-review.yml +0 -0
  13. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.github/workflows/release.yml +0 -0
  14. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.gitignore +0 -0
  15. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/.python-version +0 -0
  16. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/CODE_OF_CONDUCT.md +0 -0
  17. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/CONFIG.md +0 -0
  18. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/CONTRIBUTING.md +0 -0
  19. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/LICENSE +0 -0
  20. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/README.md +0 -0
  21. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/action.yml +0 -0
  22. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/docs/architecture.md +0 -0
  23. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/docs/design-decisions.md +0 -0
  24. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/__init__.py +0 -0
  25. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/analyzers/__init__.py +0 -0
  26. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/analyzers/ast_walker.py +0 -0
  27. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/analyzers/diff_parser.py +0 -0
  28. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/briefing/__init__.py +0 -0
  29. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/briefing/generator.py +0 -0
  30. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/cli.py +0 -0
  31. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/config.py +0 -0
  32. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/context/__init__.py +0 -0
  33. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/context/codebase_index.py +0 -0
  34. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/context/git_history.py +0 -0
  35. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/fixes/__init__.py +0 -0
  36. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/fixes/confidence.py +0 -0
  37. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/fixes/fix_generator.py +0 -0
  38. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/github_api/__init__.py +0 -0
  39. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/github_api/comment_poster.py +0 -0
  40. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/llm/__init__.py +0 -0
  41. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/llm/anthropic_provider.py +0 -0
  42. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/llm/base.py +0 -0
  43. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/llm/gemini_provider.py +0 -0
  44. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/llm/groq_provider.py +0 -0
  45. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/src/llm/ollama_provider.py +0 -0
  46. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/__init__.py +0 -0
  47. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/__init__.py +0 -0
  48. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/01-simple-refactor.json +0 -0
  49. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/02-auth-middleware.json +0 -0
  50. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/03-db-migration.json +0 -0
  51. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/04-config-update.json +0 -0
  52. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/05-public-api-deleted.json +0 -0
  53. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/06-hardcoded-api-key.json +0 -0
  54. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/07-token-in-url.json +0 -0
  55. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/08-retry-no-limit.json +0 -0
  56. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/09-missing-null-check.json +0 -0
  57. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/10-trivial-docfix.json +0 -0
  58. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/11-multi-flag.json +0 -0
  59. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/12-new-endpoint.json +0 -0
  60. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/13-auth-bypass.json +0 -0
  61. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/14-env-file-update.json +0 -0
  62. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/fixtures/15-dependency-update.json +0 -0
  63. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/rubric.md +0 -0
  64. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/eval/test_briefings.py +0 -0
  65. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/__init__.py +0 -0
  66. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_anthropic_provider.py +0 -0
  67. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_ast_walker.py +0 -0
  68. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_briefing_generator.py +0 -0
  69. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_codebase_index.py +0 -0
  70. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_config.py +0 -0
  71. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_diff_parser.py +0 -0
  72. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_failover_provider.py +0 -0
  73. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_fix_generator.py +0 -0
  74. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_gemini_provider.py +0 -0
  75. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_git_history.py +0 -0
  76. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_groq_provider.py +0 -0
  77. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/tests/unit/test_ollama_provider.py +0 -0
  78. {pr_context_engine-0.1.3 → pr_context_engine-0.1.4}/uv.lock +0 -0
@@ -6,6 +6,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Thi
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.4 — 2026-05-23
10
+
11
+ ### Fixed
12
+
13
+ - **Risk scorer false positives** — Comment lines (including `#no-space` Python comments) and bare type/struct/class declarations are now skipped before the auth-keyword check, eliminating spurious `modifies_auth` flags on struct definitions and comment blocks.
14
+ - **Auth flag noise** — `modifies_auth` is now deduplicated to one hit per file; large new store files no longer flood the briefing with dozens of identical flags.
15
+
16
+ ### Added
17
+
18
+ - **`large_new_file` flag** — New files with 300+ added lines are flagged for explicit review.
19
+ - **Briefing prompt improvements** — LLM is instructed to cover all change threads (not just the largest file), name changed type signatures explicitly, and avoid asking questions whose answers are already visible in the diff.
20
+
9
21
  ## 0.1.3 — 2026-05-23
10
22
 
11
23
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pr-context-engine
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: An AI tool that reads every PR and posts a senior-engineer-style briefing.
5
5
  Project-URL: Homepage, https://github.com/paramahastha/pr-context-engine
6
6
  Project-URL: Repository, https://github.com/paramahastha/pr-context-engine
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pr-context-engine"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "An AI tool that reads every PR and posts a senior-engineer-style briefing."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -27,6 +27,18 @@ _FUNC_DEF_RE = re.compile(
27
27
 
28
28
  _MIGRATION_MARKERS = ("migrations/", "alembic/", "alembic_migrations/")
29
29
 
30
+ # Lines that look auth-related by keyword but carry no real risk:
31
+ # - comment lines (// # /* * <!--)
32
+ # - bare type/struct/class/interface declarations
33
+ _COMMENT_LINE_RE = re.compile(r"^\s*(?://|/\*|\*+/?\s|#|<!--)")
34
+ _TYPE_DECL_RE = re.compile(
35
+ r"^\s*type\s+\w+\s+(?:struct|interface)\b" # Go struct/interface
36
+ r"|^\s*type\s+\w+\s*(?:=|\b(?:int|string|float|bool)\b)" # Go type alias/definition
37
+ r"|^\s*(?:class|interface)\s+\w+" # Python / JS / TS
38
+ )
39
+
40
+ _LARGE_NEW_FILE_THRESHOLD = 300 # added lines; new files above this warrant explicit review
41
+
30
42
 
31
43
  @dataclass
32
44
  class RiskFlag:
@@ -64,6 +76,26 @@ def _is_config(path: str) -> bool:
64
76
  return False
65
77
 
66
78
 
79
+ def _dedup_flags(flags: list[RiskFlag]) -> list[RiskFlag]:
80
+ """Deduplicate flags where one occurrence per file is enough.
81
+
82
+ modifies_auth fires once per matching line and can produce dozens of hits
83
+ in a large file (e.g. a new store implementation). Keeping only the first
84
+ match per file preserves the signal while eliminating fix-suggestion spam.
85
+ Other flag types are kept as-is because each occurrence is independently
86
+ meaningful (e.g. multiple deletes_public_api deletions).
87
+ """
88
+ seen_auth_files: set[str] = set()
89
+ result: list[RiskFlag] = []
90
+ for flag in flags:
91
+ if flag.flag == "modifies_auth":
92
+ if flag.file in seen_auth_files:
93
+ continue
94
+ seen_auth_files.add(flag.file)
95
+ result.append(flag)
96
+ return result
97
+
98
+
67
99
  def score(changes: list[FileChange]) -> list[RiskFlag]:
68
100
  """Return all risk flags detected across the list of file changes."""
69
101
  flags: list[RiskFlag] = []
@@ -79,6 +111,16 @@ def score(changes: list[FileChange]) -> list[RiskFlag]:
79
111
  RiskFlag(flag="changes_config", file=change.path, line=None, snippet=change.path)
80
112
  )
81
113
 
114
+ if change.is_new_file and len(change.added_lines) >= _LARGE_NEW_FILE_THRESHOLD:
115
+ flags.append(
116
+ RiskFlag(
117
+ flag="large_new_file",
118
+ file=change.path,
119
+ line=None,
120
+ snippet=f"{len(change.added_lines)} lines added — review for correctness and completeness",
121
+ )
122
+ )
123
+
82
124
  for hunk in change.hunks:
83
125
  new_lineno = hunk.new_start
84
126
  old_lineno = hunk.old_start
@@ -86,6 +128,10 @@ def score(changes: list[FileChange]) -> list[RiskFlag]:
86
128
  for raw in hunk.lines:
87
129
  if raw.startswith("+") and not raw.startswith("+++"):
88
130
  content = raw[1:]
131
+ stripped = content.strip()
132
+ if _COMMENT_LINE_RE.match(stripped) or _TYPE_DECL_RE.match(stripped):
133
+ new_lineno += 1
134
+ continue
89
135
  if _AUTH_RE.search(content):
90
136
  flags.append(
91
137
  RiskFlag(
@@ -118,4 +164,4 @@ def score(changes: list[FileChange]) -> list[RiskFlag]:
118
164
  new_lineno += 1
119
165
  old_lineno += 1
120
166
 
121
- return flags
167
+ return _dedup_flags(flags)
@@ -18,9 +18,13 @@ Produce a briefing with exactly four sections:
18
18
 
19
19
  1. WHAT CHANGED — 2-3 sentences. Describe the *intent* of the change, not the
20
20
  lines. Do not list files. If you can't tell the intent, say so.
21
+ If the PR has multiple distinct threads (e.g. both new infrastructure and
22
+ model/API changes), cover all threads — do not describe only the largest file.
21
23
 
22
24
  2. BLAST RADIUS — Which callers, services, contracts, or data could break?
23
25
  Be specific. If the change is internal and self-contained, write "Self-contained."
26
+ If any public-facing struct field or type signature changed, name it explicitly
27
+ (e.g. "Snapshot.Configs changed from map[string]Config to map[string]ConfigEntry").
24
28
 
25
29
  3. RISK FLAGS — Bullet list. Only include flags that are actually present.
26
30
  If none, write "None."
@@ -29,6 +33,8 @@ Produce a briefing with exactly four sections:
29
33
  approving. Questions must be answerable and specific. Bad question:
30
34
  "Did you test this?" Good question: "The new retry loop in fetch_user
31
35
  has no backoff — is that intentional given this is called per-request?"
36
+ Do NOT ask questions whose answer is already visible in the diff (e.g. do
37
+ not ask about WAL mode if the schema already sets PRAGMA journal_mode=WAL).
32
38
 
33
39
  Rules:
34
40
  - Be terse. Aim for under 200 words total.
@@ -4,6 +4,20 @@ from src.analyzers.diff_parser import FileChange, Hunk
4
4
  from src.analyzers.risk_scorer import score
5
5
 
6
6
 
7
+ def _new_file_change(path: str, num_lines: int) -> FileChange:
8
+ lines = [f"+ x_{i} = {i}" for i in range(num_lines)]
9
+ h = Hunk(old_start=0, old_count=0, new_start=1, new_count=num_lines, lines=lines)
10
+ added = [ln[1:] for ln in lines]
11
+ return FileChange(
12
+ path=path,
13
+ language="python",
14
+ added_lines=added,
15
+ removed_lines=[],
16
+ hunks=[h],
17
+ is_new_file=True,
18
+ )
19
+
20
+
7
21
  def _hunk(old_start: int, new_start: int, lines: list[str]) -> Hunk:
8
22
  added = sum(1 for ln in lines if ln.startswith("+") and not ln.startswith("+++"))
9
23
  removed = sum(1 for ln in lines if ln.startswith("-") and not ln.startswith("---"))
@@ -176,3 +190,92 @@ def test_no_flags_for_plain_change():
176
190
  c = _change("src/constants.py", [h])
177
191
  flags = score([c])
178
192
  assert flags == []
193
+
194
+
195
+ # ── comment and type-decl false-positive suppression ─────────────────────────
196
+
197
+ def test_python_comment_without_space_not_flagged():
198
+ # Previously `#\s` required a space, so `#token` slipped through.
199
+ h = _hunk(1, 1, ['+#auth_token = request.headers.get("X-Token")'])
200
+ c = _change("src/auth.py", [h])
201
+ assert not any(f.flag == "modifies_auth" for f in score([c]))
202
+
203
+
204
+ def test_python_comment_with_space_not_flagged():
205
+ h = _hunk(1, 1, ['+# store the auth token here'])
206
+ c = _change("src/auth.py", [h])
207
+ assert not any(f.flag == "modifies_auth" for f in score([c]))
208
+
209
+
210
+ def test_go_line_comment_not_flagged():
211
+ h = _hunk(1, 1, ['+// token is validated upstream'])
212
+ c = _change("pkg/handler.go", [h], language="go")
213
+ assert not any(f.flag == "modifies_auth" for f in score([c]))
214
+
215
+
216
+ def test_go_struct_decl_not_flagged():
217
+ h = _hunk(1, 1, ['+type AuthStore struct {'])
218
+ c = _change("pkg/store.go", [h], language="go")
219
+ assert not any(f.flag == "modifies_auth" for f in score([c]))
220
+
221
+
222
+ def test_python_class_decl_not_flagged():
223
+ h = _hunk(1, 1, ['+class AuthManager:'])
224
+ c = _change("src/auth.py", [h])
225
+ assert not any(f.flag == "modifies_auth" for f in score([c]))
226
+
227
+
228
+ def test_python_class_with_base_not_flagged():
229
+ h = _hunk(1, 1, ['+class AuthManager(BaseManager):'])
230
+ c = _change("src/auth.py", [h])
231
+ assert not any(f.flag == "modifies_auth" for f in score([c]))
232
+
233
+
234
+ # ── modifies_auth deduplication ───────────────────────────────────────────────
235
+
236
+ def test_auth_deduped_within_file():
237
+ lines = ['+ token_a = get_token()', '+ token_b = get_token()', '+ password = "secret"']
238
+ h = _hunk(1, 1, lines)
239
+ c = _change("src/auth.py", [h])
240
+ auth_flags = [f for f in score([c]) if f.flag == "modifies_auth"]
241
+ assert len(auth_flags) == 1
242
+
243
+
244
+ def test_auth_not_deduped_across_files():
245
+ h = _hunk(1, 1, ['+ token = get_token()'])
246
+ c1 = _change("src/auth.py", [h])
247
+ c2 = _change("src/user.py", [h])
248
+ auth_flags = [f for f in score([c1, c2]) if f.flag == "modifies_auth"]
249
+ assert len(auth_flags) == 2
250
+
251
+
252
+ def test_auth_dedup_preserves_first_match_line():
253
+ lines = ['+ password_a = "x"', '+ password_b = "y"']
254
+ h = _hunk(10, 10, lines)
255
+ c = _change("src/auth.py", [h])
256
+ auth_flags = [f for f in score([c]) if f.flag == "modifies_auth"]
257
+ assert auth_flags[0].line == 10
258
+
259
+
260
+ # ── large_new_file ────────────────────────────────────────────────────────────
261
+
262
+ def test_large_new_file_flagged():
263
+ c = _new_file_change("src/new_store.py", 300)
264
+ assert any(f.flag == "large_new_file" for f in score([c]))
265
+
266
+
267
+ def test_boundary_new_file_flagged():
268
+ c = _new_file_change("src/new_store.py", 301)
269
+ assert any(f.flag == "large_new_file" for f in score([c]))
270
+
271
+
272
+ def test_small_new_file_not_flagged():
273
+ c = _new_file_change("src/tiny.py", 50)
274
+ assert not any(f.flag == "large_new_file" for f in score([c]))
275
+
276
+
277
+ def test_large_existing_file_not_flagged():
278
+ lines = [f"+ x_{i} = {i}" for i in range(300)]
279
+ h = _hunk(1, 1, lines)
280
+ c = _change("src/existing.py", [h])
281
+ assert not any(f.flag == "large_new_file" for f in score([c]))