Skip to content

feat(case_tags): the tag vocabulary app — Tag, TagAlias, normalizer, seed - #467

Open
notashwinii wants to merge 1 commit into
mainfrom
feat/case-tags-app
Open

feat(case_tags): the tag vocabulary app — Tag, TagAlias, normalizer, seed#467
notashwinii wants to merge 1 commit into
mainfrom
feat/case-tags-app

Conversation

@notashwinii

@notashwinii notashwinii commented Aug 26, 2026

Copy link
Copy Markdown
Member

User description

Machinery for a controlled tag vocabulary. No vocabulary content, nothing reads it, Case.tags is untouched. The 121-term vocabulary.yml is the follow-up PR, kept separate so a Nepali-speaking reviewer can check labels and aliases without reading Django.

Why this exists

Case.tags is free text. Across the 82 published cases it holds 144 distinct values — including seven spellings of "illicit enrichment", four of Ncell, 21 money amounts, 2 court case numbers, and a handful of people’s names. None of it can be a public filter as it stands.

What is here

models.py Tag, TagAlias, resolve()
normalize.py the design.md §12 fold — pure functions, no DB
seed_case_tags YAML → DB, idempotent, --dry-run
tests 40, across normalize / models / seed

Three decisions worth your attention

Tag.id is the slug, not a surrogate key. It is what the search index stores, what ?tags= carries, and what a reviewer reads in the YAML. A numeric pk would put an opaque join between all three.

TagAlias.tag is nullable. Three outcomes have to stay distinguishable: resolves to a tag, was deliberately dropped, was never seen. ?tags=CIAA is a live URL today — after the cleanup it must be able to say that filter was retired rather than unknown tag, which reads as a bug to anyone holding a bookmark.

broader is capped at one level, in clean() and at seed time. Roll-up is applied by walking it at index time; a chain would make that walk unbounded and let selecting a tag silently pull in grandparents nobody chose.

The non-obvious bit of the normalizer

and arrive decomposed as + / + from legacy Preeti→Unicode conversion. They render almost identically but are different byte sequences, so those values can never match a correctly typed query — two live corpus values are affected. NFC does not fix it: + is not a canonical decomposition of , it is two distinct marks that happen to look like one. The repair is explicit and runs before anything compares strings.

Notes for review

  • Three CHECK constraints, not just clean(), because a bulk write bypasses model validation. resolve() also guards a merge cycle — unreachable in one write, but arrivable by editing two rows that were each individually fine.
  • ANN lint applies here (per-file-ignores is an opt-out list and this directory is not on it), so everything is annotated. Verified by probing that an unannotated function in this package does fail the gate.
  • Dockerfile COPY alongside the pyproject wheel entry — tests/test_app_package_names catches exactly this omission, and caught that I had done only one half.

Full suite green: 5442 passed, 5 skipped. ruff, ty and makemigrations --check all clean.

🤖 Generated with Claude Code


PR Type

Enhancement, Tests


Description

  • case_tags vocabulary app added

  • Tag normalization, resolution implemented

  • YAML seed command added

  • Model, seed tests covered


Diagram Walkthrough

flowchart LR
  A["YAML vocabulary"] -- "seeds" --> B["Tag and TagAlias"]
  B -- "normalizes aliases" --> C["resolve()"]
  C -- "returns" --> D["canonical retired unknown"]
Loading

File Walkthrough

Relevant files
Enhancement
4 files
apps.py
Add case tag app config                                                                   
+22/-0   
seed_case_tags.py
Seed YAML vocabulary into database                                             
+137/-0 
models.py
Define tags aliases resolution                                                     
+231/-0 
normalize.py
Normalize raw tag values                                                                 
+97/-0   
Database
1 files
0001_initial.py
Create tag vocabulary schema                                                         
+62/-0   
Tests
3 files
test_models.py
Test tag model invariants                                                               
+153/-0 
test_normalize.py
Test tag normalization behavior                                                   
+83/-0   
test_seed.py
Test vocabulary seed command                                                         
+161/-0 
Configuration changes
3 files
settings.py
Register `case_tags` app                                                                 
+1/-0     
Dockerfile
Include `case_tags` in image                                                         
+1/-0     
pyproject.toml
Package `case_tags` module                                                             
+1/-0     
Additional files
5 files
__init__.py [link]   
__init__.py [link]   
__init__.py [link]   
__init__.py [link]   
__init__.py [link]   


🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

model: openai/cx/gpt-5.5
git_provider: github
custom_reasoning_model: False
output_relevant_configurations: True
custom_model_max_tokens: 200000
fallback_models: ['openai/cx/gpt-5.4-mini']
ENABLE_AUTO_APPROVAL: True
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

…seed

Machinery only. No vocabulary content and nothing reads it yet; `Case.tags` is
untouched. The 123-term vocabulary.yml lands next, deliberately separate so a
Nepali-speaking reviewer can check labels and aliases without reading Django.

A separate app rather than more models in `cases`: the vocabulary is a controlled
list that happens to be applied to cases today, not a property of a case. Keeping
it apart is what lets entity or material keywords reuse it later without importing
the case model. It falls through `_db_for_label` to `default` alongside `cases`,
which it must, since resolving a case's tags will join the two.

`Tag.id` is the slug, not a surrogate key -- it is what the index stores, what
`?tags=` carries, and what a reviewer reads in the YAML. A numeric pk would put an
opaque join between all three.

`TagAlias.tag` is nullable on purpose. Three outcomes have to be distinguishable:
resolves to a tag, was deliberately dropped, was never seen. `?tags=CIAA` is a live
URL today; after the cleanup it must be able to say "that filter was removed"
rather than "unknown tag", which reads as a bug to anyone holding a bookmark. The
`dropped:` block seeds those rows.

The normalizer's non-obvious step is the Preeti repair: ो and ौ arrive DECOMPOSED
as ा+े / ा+ै from legacy Preeti->Unicode conversion. They render almost identically
but are different byte sequences, so those values can never match a correctly typed
query -- two live corpus values are affected. NFC does not fix this, because ा+े is
not a canonical decomposition of ो but two distinct marks that happen to look like
one. The repair is explicit and runs before anything compares strings.

`broader` is capped at one level, in clean() and at seed time. Roll-up is applied
by walking it at index time; a chain would mean an unbounded walk per document and
a facet where selecting a tag silently pulls in grandparents nobody chose.

Three CHECK constraints, not just clean(), because a bulk write bypasses model
validation: a merged tag must name its replacement, and neither self-reference is
allowed. resolve() also guards for a merge cycle -- unreachable through the
constraints in one write, but arrivable by editing two rows that were each fine.

ANN lint applies here (per-file-ignores is an opt-OUT list and this directory is
not on it), so everything is annotated; verified by probing that an unannotated
function in this package does fail the gate.

Dockerfile COPY alongside the pyproject wheel entry -- tests/test_app_package_names
catches exactly this, and caught that I had done only the second half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b1cec9da-ec4f-4bd0-8ec3-71064233110c


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
⚡ Recommended focus areas for review

Seed Bug

New merged tags cannot seed. First pass writes status=merged before merged_into is set, so the DB check rejects a valid YAML entry with both fields. Include merged_into during create/update or stage status after relations.

_, was_created = Tag.objects.update_or_create(
    pk=entry["id"],
    defaults={
        "label_ne": entry["label_ne"],
        "label_en": entry["label_en"],
        "status": entry.get("status", TagStatus.PROPOSED),
        "sort_order": entry.get("sort_order", 0),
        "note": entry.get("note", "") or "",
    },
)

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
model: openai/cx/gpt-5.5
git_provider: github
custom_reasoning_model: False
output_relevant_configurations: True
custom_model_max_tokens: 200000
fallback_models: ['openai/cx/gpt-5.4-mini']
ENABLE_AUTO_APPROVAL: True
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
Reject alias conflicts

Duplicate normalized aliases silently overwrite earlier tags. Reject conflicting key
mappings so vocabulary meaning cannot depend on YAML order.

case_tags/management/commands/seed_case_tags.py [103-105]

 for entry in entries:
     for raw in [*(entry.get("aliases") or []), entry["id"]]:
-        wanted[normalize(str(raw))] = (entry["id"], "", str(raw))
+        key = normalize(str(raw))
+        if key in wanted and wanted[key][0] != entry["id"]:
+            raise CommandError(
+                f"{raw!r} aliases both {wanted[key][0]!r} and {entry['id']!r}"
+            )
+        wanted[key] = (entry["id"], "", str(raw))
Suggestion importance[1-10]: 6

__

Why: Valid guard. Duplicate normalized aliases currently overwrite by YAML order, risking wrong TagAlias mappings.

Low

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
model: openai/cx/gpt-5.5
git_provider: github
custom_reasoning_model: False
output_relevant_configurations: True
custom_model_max_tokens: 200000
fallback_models: ['openai/cx/gpt-5.4-mini']
ENABLE_AUTO_APPROVAL: True
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