Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@
## 2026-05-18 - Jaccard Similarity Optimization via Set Arithmetic
**Learning:** In retrieval loops calculating Jaccard similarity (e.g. RAG), explicitly building a union set `A.union(B)` is expensive due to memory allocation and population.
**Action:** Use the inclusion-exclusion principle $|A \cup B| = |A| + |B| - |A \cap B|$ to calculate union size in O(1) arithmetic time after calculating the intersection. Pre-calculate $|B|$ (token count) to further reduce overhead. Use `isdisjoint()` for fast early-exit.

## 2026-05-19 - Replace func.sum(case(...)) with GROUP BY
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the learning entry date to match this PR’s actual timeline.

This note is dated 2026-05-19, but this PR was opened on 2026-05-14. Future-dating makes the learning log harder to trust and search.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/bolt.md at line 93, Update the learning entry header "## 2026-05-19 -
Replace func.sum(case(...)) with GROUP BY" to the PR's actual opening date
(change 2026-05-19 to 2026-05-14) so the entry date matches this PR's timeline
and avoids future-dating the log; locate the header line with the exact phrase
"Replace func.sum(case(...)) with GROUP BY" and edit only the date portion.

**Learning:** Using multiple `func.sum(case(...))` aggregates over a single categorical column performs significantly slower than a simple `GROUP BY` query, as the database evaluates each condition for every row instead of optimizing via index/hashing.
**Action:** Replace `func.sum(case(...))` aggregation logic with a standard `GROUP BY` query and load the results into a Python dictionary for O(1) value lookups.
184 changes: 114 additions & 70 deletions backend/closure_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, timedelta, timezone
from backend.models import Grievance, GrievanceFollower, ClosureConfirmation, GrievanceStatus
from backend.models import (
Grievance,
GrievanceFollower,
ClosureConfirmation,
GrievanceStatus,
)
import logging
import hashlib
import hmac
Expand All @@ -10,102 +15,125 @@

logger = logging.getLogger(__name__)


class ClosureService:
"""Service for handling grievance closure confirmation logic"""

# Configuration
CONFIRMATION_THRESHOLD = 0.60 # 60% of followers must confirm
TIMEOUT_DAYS = 7 # 7 days to confirm
MINIMUM_FOLLOWERS = 3 # Minimum followers needed for confirmation process

@staticmethod
def request_closure(grievance_id: int, db: Session) -> dict:
"""Request closure for a grievance - triggers confirmation process"""
grievance = db.query(Grievance).filter(Grievance.id == grievance_id).first()
if not grievance:
raise ValueError("Grievance not found")

if grievance.status == GrievanceStatus.RESOLVED:
raise ValueError("Grievance is already resolved")

# Count followers
follower_count = db.query(func.count(GrievanceFollower.id)).filter(
GrievanceFollower.grievance_id == grievance_id
).scalar()

follower_count = (
db.query(func.count(GrievanceFollower.id))
.filter(GrievanceFollower.grievance_id == grievance_id)
.scalar()
)

# If less than minimum followers, skip confirmation process
if follower_count < ClosureService.MINIMUM_FOLLOWERS:
grievance.status = GrievanceStatus.RESOLVED
grievance.resolved_at = datetime.now(timezone.utc)
grievance.closure_approved = True
db.commit()

return {
"message": "Grievance resolved (no confirmation needed - insufficient followers)",
"skip_confirmation": True,
"follower_count": follower_count
"follower_count": follower_count,
}

# Set closure pending
grievance.pending_closure = True
grievance.closure_requested_at = datetime.now(timezone.utc)
grievance.closure_confirmation_deadline = datetime.now(timezone.utc) + timedelta(days=ClosureService.TIMEOUT_DAYS)
grievance.closure_confirmation_deadline = datetime.now(
timezone.utc
) + timedelta(days=ClosureService.TIMEOUT_DAYS)
db.commit()

required_confirmations = max(1, int(follower_count * ClosureService.CONFIRMATION_THRESHOLD))


required_confirmations = max(
1, int(follower_count * ClosureService.CONFIRMATION_THRESHOLD)
)
Comment on lines +65 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use ceil, not int, for confirmation threshold math.

int() floors the requirement and under-enforces the 60% rule (e.g., 3 followers requires 1 confirmation). This can approve closures prematurely.

Suggested patch
+import math
...
-        required_confirmations = max(
-            1, int(follower_count * ClosureService.CONFIRMATION_THRESHOLD)
-        )
+        required_confirmations = max(
+            1, math.ceil(follower_count * ClosureService.CONFIRMATION_THRESHOLD)
+        )
...
-        required_confirmations = max(
-            1, int(total_followers * ClosureService.CONFIRMATION_THRESHOLD)
-        )
+        required_confirmations = max(
+            1, math.ceil(total_followers * ClosureService.CONFIRMATION_THRESHOLD)
+        )

Also applies to: 187-189

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/closure_service.py` around lines 65 - 67, The calculation for
required_confirmations in ClosureService uses int(...) which floors and can
undercount confirmations; change the logic in the places using
int(follower_count * ClosureService.CONFIRMATION_THRESHOLD) (e.g., where
required_confirmations is computed) to use math.ceil(...) to round up instead,
and add the math import if missing so the required_confirmations = max(1,
math.ceil(follower_count * ClosureService.CONFIRMATION_THRESHOLD)) enforces the
60% rule; update both the occurrence around the required_confirmations
assignment shown and the similar calculation at the later occurrence (lines
~187-189).


return {
"message": "Closure confirmation requested - waiting for community approval",
"skip_confirmation": False,
"follower_count": follower_count,
"required_confirmations": required_confirmations,
"deadline": grievance.closure_confirmation_deadline
"deadline": grievance.closure_confirmation_deadline,
}

@staticmethod
def submit_confirmation(grievance_id: int, user_email: str, confirmation_type: str, reason: str, db: Session) -> dict:
def submit_confirmation(
grievance_id: int,
user_email: str,
confirmation_type: str,
reason: str,
db: Session,
) -> dict:
"""Submit a closure confirmation or dispute"""
grievance = db.query(Grievance).filter(Grievance.id == grievance_id).first()
if not grievance:
raise ValueError("Grievance not found")

if not grievance.pending_closure:
raise ValueError("Grievance is not pending closure confirmation")

# Check if user is a follower
is_follower = db.query(GrievanceFollower).filter(
GrievanceFollower.grievance_id == grievance_id,
GrievanceFollower.user_email == user_email
).first()

is_follower = (
db.query(GrievanceFollower)
.filter(
GrievanceFollower.grievance_id == grievance_id,
GrievanceFollower.user_email == user_email,
)
.first()
)

if not is_follower:
raise ValueError("Only followers can confirm or dispute closure")

# Check if user already submitted confirmation
existing = db.query(ClosureConfirmation).filter(
ClosureConfirmation.grievance_id == grievance_id,
ClosureConfirmation.user_email == user_email
).first()

existing = (
db.query(ClosureConfirmation)
.filter(
ClosureConfirmation.grievance_id == grievance_id,
ClosureConfirmation.user_email == user_email,
)
.first()
)

if existing:
raise ValueError("You have already submitted a response for this closure")

# Blockchain feature: calculate integrity hash for the closure confirmation
# Performance Boost: Use thread-safe cache to eliminate DB query for last hash
prev_hash = closure_last_hash_cache.get("last_hash")
if prev_hash is None:
# Cache miss: Fetch only the last hash from DB
last_record = db.query(ClosureConfirmation.integrity_hash).order_by(ClosureConfirmation.id.desc()).first()
last_record = (
db.query(ClosureConfirmation.integrity_hash)
.order_by(ClosureConfirmation.id.desc())
.first()
)
prev_hash = last_record[0] if last_record and last_record[0] else ""
closure_last_hash_cache.set(data=prev_hash, key="last_hash")

# Chaining logic: hash(grievance_id|user_email|confirmation_type|prev_hash)
hash_content = f"{grievance_id}|{user_email}|{confirmation_type}|{prev_hash}"
secret_key = get_auth_config().secret_key
integrity_hash = hmac.new(
secret_key.encode('utf-8'),
hash_content.encode('utf-8'),
hashlib.sha256
secret_key.encode("utf-8"), hash_content.encode("utf-8"), hashlib.sha256
).hexdigest()

# Create confirmation record
Expand All @@ -115,7 +143,7 @@ def submit_confirmation(grievance_id: int, user_email: str, confirmation_type: s
confirmation_type=confirmation_type,
reason=reason,
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash
previous_integrity_hash=prev_hash,
)
db.add(confirmation)
db.commit()
Expand All @@ -125,76 +153,92 @@ def submit_confirmation(grievance_id: int, user_email: str, confirmation_type: s

# Check if threshold is met
return ClosureService.check_and_finalize_closure(grievance_id, db)

@staticmethod
def check_and_finalize_closure(grievance_id: int, db: Session) -> dict:
"""Check if closure threshold is met and finalize if needed"""
grievance = db.query(Grievance).filter(Grievance.id == grievance_id).first()
if not grievance or not grievance.pending_closure:
return {"closure_finalized": False}

# Count followers and confirmations
total_followers = db.query(func.count(GrievanceFollower.id)).filter(
GrievanceFollower.grievance_id == grievance_id
).scalar()

total_followers = (
db.query(func.count(GrievanceFollower.id))
.filter(GrievanceFollower.grievance_id == grievance_id)
.scalar()
)

# Get all confirmation counts in a single query instead of multiple round-trips
from sqlalchemy import case
stats = db.query(
func.sum(case((ClosureConfirmation.confirmation_type == 'confirmed', 1), else_=0)).label('confirmed'),
func.sum(case((ClosureConfirmation.confirmation_type == 'disputed', 1), else_=0)).label('disputed')
).filter(ClosureConfirmation.grievance_id == grievance_id).first()

confirmations_count = stats.confirmed or 0
disputes_count = stats.disputed or 0

required_confirmations = max(1, int(total_followers * ClosureService.CONFIRMATION_THRESHOLD))

# Optimized: Replace expensive func.sum(case(...)) with a standard GROUP BY
counts = (
db.query(
ClosureConfirmation.confirmation_type,
func.count(ClosureConfirmation.id),
)
.filter(ClosureConfirmation.grievance_id == grievance_id)
.group_by(ClosureConfirmation.confirmation_type)
.all()
)
counts_dict = dict(counts)

confirmations_count = counts_dict.get("confirmed", 0)
disputes_count = counts_dict.get("disputed", 0)

required_confirmations = max(
1, int(total_followers * ClosureService.CONFIRMATION_THRESHOLD)
)

# Check if threshold is met
if confirmations_count >= required_confirmations:
grievance.status = GrievanceStatus.RESOLVED
grievance.resolved_at = datetime.now(timezone.utc)
grievance.closure_approved = True
grievance.pending_closure = False
db.commit()

return {
"closure_finalized": True,
"approved": True,
"confirmations": confirmations_count,
"required": required_confirmations,
"message": "Grievance closure approved by community"
"message": "Grievance closure approved by community",
}

return {
"closure_finalized": False,
"confirmations": confirmations_count,
"disputes": disputes_count,
"required": required_confirmations,
"total_followers": total_followers
"total_followers": total_followers,
}

@staticmethod
def check_timeout_and_finalize(db: Session):
"""Background task to check for timed-out closure requests"""
now = datetime.now(timezone.utc)

# Find grievances with expired deadlines
expired_grievances = db.query(Grievance).filter(
Grievance.pending_closure == True,
Grievance.closure_confirmation_deadline < now
).all()

expired_grievances = (
db.query(Grievance)
.filter(
Grievance.pending_closure == True,
Grievance.closure_confirmation_deadline < now,
)
.all()
)

for grievance in expired_grievances:
# Check current status
result = ClosureService.check_and_finalize_closure(grievance.id, db)

if not result.get("closure_finalized"):
# Timeout - log dispute and keep open
logger.warning(f"Grievance {grievance.id} closure timeout - threshold not met")
logger.warning(
f"Grievance {grievance.id} closure timeout - threshold not met"
)
grievance.pending_closure = False
grievance.closure_approved = False
# Keep status as is (not resolved)
db.commit()
return len(expired_grievances)

return len(expired_grievances)
Loading
Loading