Skip to content

refactor(casework): one evidence normaliser, not two copies - #440

Open
gaurav-karki wants to merge 2 commits into
mainfrom
fix/casework-shared-evidence
Open

refactor(casework): one evidence normaliser, not two copies#440
gaurav-karki wants to merge 2 commits into
mainfrom
fix/casework-shared-evidence

Conversation

@gaurav-karki

@gaurav-karki gaurav-karki commented Aug 8, 2026

Copy link
Copy Markdown
Member

User description

Follow-up to @damo-da's review comment on #429:

I don't like this. The script can import django modules but not the other way around.

current_evidence and merge_evidence existed twice — once in bind_materials.py,
once in enrich_news_articles.py — with a docstring justifying the copy because
"these are standalone scripts with no shared sequencing".

That reason does not hold, and the files disprove it themselves. A casework script
may import from shared and app modules; only the reverse is forbidden.
enrich_news_articles already imports materials.jsonld, a Django app module, and
both scripts already import half of casework/common/.

What changed

Both helpers now live in casework/common/evidence.py and both writers import them.
Net −24 lines.

The copies were not identical: the binder appended additional_details: "", the news
stage appended a real Nepali note. The shared merge_evidence takes
(material_iri, note) pairs, so the binder passes "" at its own call site. The
difference is now one visible argument instead of a second function.

Why it is worth doing

PATCH /evidence is a destructive whole-list replace — the body is the new list,
so a normaliser that drops a field drops evidence rows from a published case. The old
arrangement guarded that with a comment asking the next person to keep two copies in
sync.

The test that checked the comment existed is replaced by one that checks the thing
that matters:

assert en.current_evidence is shared.current_evidence
assert bind_materials.merge_evidence is shared.merge_evidence

Identity, not similarity. Two copies can drift; the same function object cannot.

Second commit: what code review caught

casework/README.md teaches the canonical /evidence write, and its snippet still
passed bare IRI strings — ValueError: too many values to unpack against the new
signature. The page whose job is to stop people destroying an evidence list handed
them code that does not run. Its import also still named casework.bind_materials,
which kept working as a re-export and would have gone on pointing readers at the old
home.

The quieter half is the worse one. A 2-character string unpacked into
iri="a", note="b" and bound silently. Real IRIs are long so it never fires by
accident, but a function guarding a destructive write should not have a shape of input
it accepts and corrupts. merge_evidence now rejects a bare string with a message
naming the fix.

Verification

uv run pytest -q --ignore=integration-tests   # 4,619 passed, 5 skipped
uv run ruff check .                           # clean
uv run ty check                               # clean

One thing to confirm

The shared code went into casework/common/, next to its only two callers and beside
the other shared casework helpers. Your comment mentions django modules, so if you
meant jawafdehi_shared/ instead, that is a one-file move — say so and I will redo it.

🤖 Generated with Claude Code


PR Type

Enhancement, Tests, Documentation


Description

  • Share /evidence merge helpers

  • Preserve note differences via arguments

  • Reject bare IRI additions

  • Update tests, README recipe


Diagram Walkthrough

flowchart LR
  A["bind_materials"] -- "imports" --> B["common.evidence"]
  C["enrich_news_articles"] -- "imports" --> B
  B -- "normalizes" --> D["PATCH /evidence payload"]
  E["tests"] -- "assert identity" --> B
Loading

File Walkthrough

Relevant files
Enhancement
3 files
bind_materials.py
Import shared evidence helpers                                                     
+3/-39   
evidence.py
Add shared evidence utilities                                                       
+35/-0   
enrich_news_articles.py
Reuse shared evidence merging                                                       
+5/-43   
Documentation
2 files
news_search.py
Clarify blank note rationale                                                         
+3/-3     
README.md
Update evidence merge recipe                                                         
+6/-5     
Tests
2 files
test_bind_materials.py
Cover pair-based evidence merge                                                   
+13/-3   
test_enrich_news_articles.py
Assert shared helper identity                                                       
+14/-10 


🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
enable_ai_metadata: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_description]

publish_labels: False
add_original_user_description: True
generate_ai_title: False
use_bullet_points: True
extra_instructions: 
enable_pr_type: True
final_update_message: True
enable_help_text: False
enable_help_comment: False
enable_pr_diagram: True
publish_description_as_comment: False
publish_description_as_comment_persistent: True
enable_semantic_files_types: True
collapsible_file_list: adaptive
collapsible_file_list_threshold: 6
inline_file_summary: False
use_description_markers: False
enable_large_pr_handling: True
include_generated_by_header: True
max_ai_calls: 4
async_ai_calls: True

Summary by CodeRabbit

  • Bug Fixes

    • Evidence records now preserve existing notes and ordering when new materials are added.
    • Duplicate materials are skipped automatically.
    • Missing evidence notes are handled consistently with an empty note.
    • Invalid evidence entries now produce clearer validation errors.
  • Documentation

    • Updated evidence-writing guidance and evidence summary requirements for greater clarity.
  • Tests

    • Expanded coverage for evidence merging, duplicate handling, note preservation, and invalid inputs.

gaurav-karki and others added 2 commits August 8, 2026 00:25
Review feedback on #429: the duplication was justified in a docstring with
"these are standalone scripts", and that reason does not hold. A casework
script may import from shared and app modules; only the reverse is forbidden.
Both scripts already do it -- `enrich_news_articles` imports
`materials.jsonld`, and both import half of `casework/common/`.

`current_evidence` and `merge_evidence` now live in `casework/common/evidence.py`
and both writers import them. The copies differed in one respect: the binder
appended `additional_details: ""` while the news stage appended a real note. The
shared `merge_evidence` takes `(material_iri, note)` pairs, so the binder passes
`""` explicitly at its call site rather than the difference living in a second
function.

This matters because `PATCH /evidence` is a destructive whole-list replace: the
body IS the new list, so a normaliser that drops a field drops evidence rows
from a published case. The old arrangement guarded that with a comment asking
the next person to keep two copies in sync. The test that checked the comment
existed is replaced by one asserting the two modules reference the SAME function
objects -- identity, which cannot drift, instead of similarity, which can.

Trimmed the docstrings on the lines this touched, per the new rule in CLAUDE.md,
including two more `file.py:NNN` references.

4,618 passed, 5 skipped; ruff and ty clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code review on the previous commit: `casework/README.md` teaches the canonical
`/evidence` write, and its snippet still passed bare IRI strings. Against the
shared `merge_evidence` that is `ValueError: too many values to unpack` on the
first real IRI -- so the one page whose job is to stop people destroying an
evidence list handed them code that does not run. Its import also still named
`casework.bind_materials`, which kept working as a re-export and would have gone
on pointing readers at the old home.

The review also spotted the quiet half, which is the worse one: a 2-character
string unpacks into `iri="a", note="b"` and binds silently. Real IRIs are long
so this never fires by accident, but a function guarding a destructive
whole-list replace should not have a shape of input it accepts and corrupts.
`merge_evidence` now rejects a bare string with a message naming the fix.
Verified both: the long IRI and the 2-character one now raise, pairs are
unaffected.

Dropped a `file.py:NNN` reference in the same README paragraph while there.

4,619 passed, 5 skipped; ruff and ty clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e7c18ec-3db9-4fbc-8bf7-35aea7eef2ac

📥 Commits

Reviewing files that changed from the base of the PR and between 0800f44 and 1c40ed3.

📒 Files selected for processing (7)
  • casework/README.md
  • casework/bind_materials.py
  • casework/common/evidence.py
  • casework/enrich_news_articles.py
  • casework/news_search.py
  • tests/casework/test_bind_materials.py
  • tests/casework/test_enrich_news_articles.py

📝 Walkthrough

Walkthrough

The change adds shared evidence normalization and merge helpers. Material binding and news enrichment now use these helpers. Tests validate tuple-based evidence additions, duplicate handling, note preservation, invalid inputs, and shared helper usage.

Changes

Evidence utility consolidation

Layer / File(s) Summary
Shared evidence contract
casework/common/evidence.py
Adds current_evidence and merge_evidence for normalized evidence entries, unique material IRIs, preserved notes, and validated tuple inputs.
Casework integrations
casework/bind_materials.py, casework/enrich_news_articles.py, casework/README.md, casework/news_search.py
Updates material binding and news enrichment to use the shared helpers. Bind-time additions pass empty notes. Documentation describes the shared API and bound evidence notes.
Evidence validation
tests/casework/test_bind_materials.py, tests/casework/test_enrich_news_articles.py
Updates merge tests for tuple inputs and verifies invalid strings, note preservation, and shared helper identity.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: jawafdehi-pr-agent

Poem

A rabbit checked the notes with care,
And found one helper waiting there.
Tuples hop in, duplicates stay,
Empty notes mark the binders’ way.
Shared evidence now leads the trail.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: consolidating duplicate evidence normalization helpers into one shared implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/casework-shared-evidence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_reviewer]

require_ticket_analysis_review: False
require_score_review: False
require_tests_review: True
require_estimate_effort_to_review: True
require_can_be_split_review: False
require_security_review: True
require_estimate_contribution_time_cost: False
require_todo_scan: False
publish_output_no_suggestions: True
persistent_comment: True
extra_instructions: Focus on: logic errors and edge cases; security/authz regressions; missing error handling;
Django/DRF correctness (migrations, N+1 queries, transaction/atomicity, serializer & permission gaps).
Do NOT comment on formatting, import order, or naming — ruff handles those in CI.

num_max_findings: 3
final_update_message: True
enable_review_labels_security: True
enable_review_labels_effort: True
require_all_thresholds_for_incremental_review: False
minimal_commits_for_incremental_review: 0
minimal_minutes_for_incremental_review: 0
enable_intro_text: True
enable_help_text: False

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate evidence pair shape

merge_evidence still silently accepts dicts or other 2-item iterables, producing
garbage material_iri values like "material_iri". Validate explicit pair shape before
append.

casework/common/evidence.py [23-34]

 for addition in additions:
     # A bare IRI is the natural mistake here, and a 2-character string would
     # unpack into iri="a", note="b" and bind silently. Say so instead.
-    if isinstance(addition, str):
+    if (
+        isinstance(addition, str)
+        or not isinstance(addition, tuple)
+        or len(addition) != 2
+    ):
         raise TypeError(
-            f"merge_evidence takes (material_iri, note) pairs, got the bare "
-            f"string {addition!r}. Pass [(iri, '')] if there is no note.")
+            f"merge_evidence takes (material_iri, note) pairs, got "
+            f"{addition!r}. Pass [(iri, '')] if there is no note.")
     iri, note = addition
     if iri in have:
         continue
-    merged.append({"material_iri": iri, "additional_details": note})
+    merged.append({"material_iri": iri, "additional_details": note or ""})
     have.add(iri)
Suggestion importance[1-10]: 6

__

Why: Valid concern: merge_evidence can unpack dict keys or arbitrary 2-item iterables into bad material_iri data. Impact moderate; improved code over-restricts to tuple, rejecting valid pair-like lists.

Low

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_code_suggestions]

commitable_code_suggestions: False
dual_publishing_score_threshold: -1
focus_only_on_problems: True
extra_instructions: Prefer a few high-impact, project-specific suggestions over many generic ones.
Skip style/formatting (ruff-enforced) and changes under cases/migrations/.

enable_help_text: False
enable_chat_text: False
persistent_comment: True
max_history_len: 4
publish_output_no_suggestions: True
suggestions_score_threshold: 0
new_score_mechanism: True
new_score_mechanism_th_high: 9
new_score_mechanism_th_medium: 7
auto_extended_mode: True
num_code_suggestions_per_chunk: 3
max_number_of_calls: 3
parallel_calls: True
final_clip_factor: 0.8
decouple_hunks: False
demand_code_suggestions_self_review: False
code_suggestions_self_review_text: **Author self-review**: I have reviewed the PR code suggestions, and addressed the relevant ones.
approve_pr_on_self_review: False
fold_suggestions_on_self_review: True
num_code_suggestions: 4

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Agent Walkthrough 🤖

Welcome to the PR Agent, an AI-powered tool for automated pull request analysis, feedback, suggestions and more.

Here is a list of tools you can use to interact with the PR Agent:

ToolDescriptionTrigger Interactively 💎

DESCRIBE

Generates PR description - title, type, summary, code walkthrough and labels
  • Run

REVIEW

Adjustable feedback about the PR, possible issues, security concerns, review effort and more
  • Run

IMPROVE

Code suggestions for improving the PR
  • Run

UPDATE CHANGELOG

Automatically updates the changelog
  • Run

HELP DOCS

Answers a question regarding this repository, or a given one, based on given documentation path
  • Run

ADD DOCS

Generates documentation to methods/functions/classes that changed in the PR
  • Run

ASK

Answering free-text questions about the PR

[*]

GENERATE CUSTOM LABELS

Generates custom labels for the PR, based on specific guidelines defined by the user

[*]

(1) Note that each tool can be triggered automatically when a new PR is opened, or called manually by commenting on a PR.

(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the /ask tool, you need to comment on a PR: /ask "<question content>". See the relevant documentation for each tool for more details.

@jawafdehi-pr-agent

Copy link
Copy Markdown

Auto-approved PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant