Skip to content
Merged
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
59 changes: 49 additions & 10 deletions src/gradata/_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,35 @@ class RuleScope:
channel: str = ""
stakes: str = "normal"
agent_type: str = "" # Agent type for scoped rule injection (e.g. "researcher", "reviewer")
namespace: str = "" # Scope tag for per-context rules (e.g. "api-endpoint", "onboarding")
namespace: str = "" # Scope tag for per-context rules (e.g. "api-endpoint", "onboarding")
temporal_relevance: str = "" # "evergreen", "seasonal", "recent", or "" (wildcard)
max_idle_sessions: int = 0 # Auto-suppress after N idle sessions (0 = never)
created_session: int = 0 # Session number when this scope was first assigned


def temporal_decay(
sessions_since_fire: int,
max_idle: int,
floor: float = 0.05,
steepness: float = 3.0,
) -> float:
"""Compute temporal decay multiplier for rule confidence.

Uses exponential decay: exp(-steepness * ratio^2) where
ratio = sessions_since_fire / max_idle.

Returns decay multiplier in [floor, 1.0]. If max_idle=0, returns 1.0 (evergreen).
"""
if max_idle <= 0:
return 1.0
if sessions_since_fire <= 0:
return 1.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 import math inside function body

math is a stdlib module and can be imported at the top of the file without any cost. Placing the import inside the function body is unusual for a non-optional/lazy dependency and can confuse static analysis tools.

Move this to the top-level imports alongside the other stdlib imports.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gradata/_scope.py
Line: 71

Comment:
**`import math` inside function body**

`math` is a stdlib module and can be imported at the top of the file without any cost. Placing the import inside the function body is unusual for a non-optional/lazy dependency and can confuse static analysis tools.

Move this to the top-level imports alongside the other stdlib imports.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code


import math
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider moving import math to module level.

The math import inside the function incurs a small lookup overhead on each call. Since math is a standard library module with no circular dependency concerns, moving it to the module-level imports improves clarity and efficiency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gradata/_scope.py` at line 73, Move the local "import math" out of the
function and place it with the module-level imports at the top of
src/gradata/_scope.py; locate the inline "import math" occurrence (the one shown
in the diff) and remove it from inside the function, then add a single
module-level import line "import math" so functions in this module can reference
math without repeated lookup overhead.


ratio = sessions_since_fire / max_idle
decay = math.exp(-steepness * ratio * ratio)
return max(floor, round(decay, 4))
Comment on lines +55 to +77
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

Add upper bound clamp to temporal_decay return value.

The docstring promises a return value in [floor, 1.0], but if floor > 1.0 is passed (invalid but possible), the function returns > 1.0. This could cause downstream issues if the multiplier is used with confidence values that must stay in [0.0, 1.0].

Proposed defensive fix
     ratio = sessions_since_fire / max_idle
     decay = math.exp(-steepness * ratio * ratio)
-    return max(floor, round(decay, 4))
+    return min(1.0, max(floor, round(decay, 4)))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gradata/_scope.py` around lines 55 - 77, temporal_decay can return >1.0
if a caller passes floor > 1.0; clamp the final returned multiplier to the
promised upper bound of 1.0 by ensuring the return value is min(1.0, max(floor,
round(decay, 4))). Update the return expression in temporal_decay (which uses
sessions_since_fire, max_idle, floor, steepness and computes decay) so the
result is always within [floor, 1.0] (effectively [min(floor,1.0), 1.0] when
floor > 1.0).



# ---------------------------------------------------------------------------
Expand All @@ -56,7 +84,7 @@ class RuleScope:
# task_type → keywords (any keyword triggers the type; first match wins)
_TASK_TYPE_KEYWORDS: list[tuple[str, list[str]]] = [
("email_draft", ["email", "draft", "write", "compose", "reply", "follow-up", "followup"]),
("demo_prep", ["demo", "call", "meeting", "prep", "presentation"]),
("demo_prep", ["demo", "call", "meeting", "prep", "presentation"]),
("prospecting", ["prospect", "lead", "find", "enrich", "list", "sweep"]),
("code_review", ["review", "code", "pr", "pull request"]),
("documentation", ["doc", "readme", "guide", "spec"]),
Expand All @@ -65,24 +93,24 @@ class RuleScope:
# audience → title keywords (checked case-insensitively; first match wins)
_AUDIENCE_KEYWORDS: list[tuple[str, list[str]]] = [
("c_suite", ["ceo", "cto", "coo", "cfo", "cro", "cmo", "chief"]),
("vp", ["vp", "vice president", "head of"]),
("vp", ["vp", "vice president", "head of"]),
("director", ["director"]),
("manager", ["manager"]),
("manager", ["manager"]),
# "ic" is the fallback for anything unmatched
]

# channel inference: task keywords → channel
_CHANNEL_KEYWORDS: list[tuple[str, list[str]]] = [
("email", ["email", "draft", "compose", "reply", "follow-up", "followup"]),
("slack", ["slack", "message", "dm"]),
("email", ["email", "draft", "compose", "reply", "follow-up", "followup"]),
("slack", ["slack", "message", "dm"]),
("document", ["doc", "readme", "guide", "spec", "report"]),
("call", ["call", "meeting", "demo", "presentation"]),
("call", ["call", "meeting", "demo", "presentation"]),
]

# stakes inference: task keywords → stakes override
_STAKES_KEYWORDS: list[tuple[str, list[str]]] = [
("high", ["demo", "call", "meeting", "presentation", "proposal", "critical"]),
("low", ["internal", "draft", "note"]),
("high", ["demo", "call", "meeting", "presentation", "proposal", "critical"]),
("low", ["internal", "draft", "note"]),
]


Expand Down Expand Up @@ -294,5 +322,16 @@ def scope_from_dict(data: dict[str, str]) -> RuleScope:
channel='', stakes='high')
"""
valid_fields = {f for f in RuleScope.__dataclass_fields__} # type: ignore[attr-defined]
filtered = {k: v for k, v in data.items() if k in valid_fields}
# Coerce int fields that were stringified by scope_to_dict
_INT_FIELDS = {"max_idle_sessions", "created_session"}
filtered = {}
for k, v in data.items():
if k not in valid_fields:
continue
if k in _INT_FIELDS:
try:
v = int(v)
except (ValueError, TypeError):
v = 0
filtered[k] = v
return RuleScope(**filtered)
Loading
Loading