onecoder 0.0.2__py3-none-any.whl

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 (73) hide show
  1. onecoder/agent.py +95 -0
  2. onecoder/agentic_tool_search/__init__.py +0 -0
  3. onecoder/agentic_tool_search/dynamic_tool_search.py +64 -0
  4. onecoder/agentic_tool_search/registry.py +33 -0
  5. onecoder/agents/__init__.py +7 -0
  6. onecoder/agents/documentation_agent.py +12 -0
  7. onecoder/agents/file_reader_agent.py +19 -0
  8. onecoder/agents/file_writer_agent.py +19 -0
  9. onecoder/agents/orchestrator_agent.py +51 -0
  10. onecoder/agents/refactoring_agent.py +12 -0
  11. onecoder/agents/research_agent.py +31 -0
  12. onecoder/agents/task_suggestion_agent.py +88 -0
  13. onecoder/alignment.py +236 -0
  14. onecoder/api.py +162 -0
  15. onecoder/api_client.py +112 -0
  16. onecoder/backends/base.py +22 -0
  17. onecoder/backends/local_tui.py +65 -0
  18. onecoder/blackboard.py +102 -0
  19. onecoder/cli.py +108 -0
  20. onecoder/commands/__init__.py +1 -0
  21. onecoder/commands/auth.py +78 -0
  22. onecoder/commands/ci.py +29 -0
  23. onecoder/commands/delegate.py +557 -0
  24. onecoder/commands/doctor.py +40 -0
  25. onecoder/commands/issue.py +136 -0
  26. onecoder/commands/logs.py +45 -0
  27. onecoder/commands/project.py +270 -0
  28. onecoder/commands/server.py +170 -0
  29. onecoder/config_manager.py +87 -0
  30. onecoder/constants.py +9 -0
  31. onecoder/diagnostics/__init__.py +2 -0
  32. onecoder/diagnostics/env_scan.py +207 -0
  33. onecoder/discovery.py +101 -0
  34. onecoder/distillation.py +236 -0
  35. onecoder/evaluation/__init__.py +1 -0
  36. onecoder/evaluation/ttu.py +176 -0
  37. onecoder/governance/__init__.py +0 -0
  38. onecoder/governance/probllm.py +91 -0
  39. onecoder/hooks.py +74 -0
  40. onecoder/ipc_auth.py +200 -0
  41. onecoder/issues.py +188 -0
  42. onecoder/jules_client.py +343 -0
  43. onecoder/knowledge.py +106 -0
  44. onecoder/llm.py +61 -0
  45. onecoder/logger.py +42 -0
  46. onecoder/metrics.py +129 -0
  47. onecoder/models/delegation.py +46 -0
  48. onecoder/onboarding.py +264 -0
  49. onecoder/review.py +233 -0
  50. onecoder/services/delegation_service.py +209 -0
  51. onecoder/services/validation_service.py +104 -0
  52. onecoder/sessions.py +186 -0
  53. onecoder/sprint_collector.py +165 -0
  54. onecoder/sync.py +167 -0
  55. onecoder/tmux.py +86 -0
  56. onecoder/tools/__init__.py +10 -0
  57. onecoder/tools/executor.py +53 -0
  58. onecoder/tools/external_tools.py +106 -0
  59. onecoder/tools/file_tools.py +77 -0
  60. onecoder/tools/interface.py +25 -0
  61. onecoder/tools/jules_tools.py +122 -0
  62. onecoder/tools/kit_tools.py +122 -0
  63. onecoder/tools/registry.py +32 -0
  64. onecoder/tui/__init__.py +5 -0
  65. onecoder/tui/app.py +263 -0
  66. onecoder/tui/commands.py +150 -0
  67. onecoder/tui/widgets.py +92 -0
  68. onecoder/worktree.py +186 -0
  69. onecoder-0.0.2.dist-info/METADATA +17 -0
  70. onecoder-0.0.2.dist-info/RECORD +73 -0
  71. onecoder-0.0.2.dist-info/WHEEL +5 -0
  72. onecoder-0.0.2.dist-info/entry_points.txt +2 -0
  73. onecoder-0.0.2.dist-info/top_level.txt +1 -0
onecoder/review.py ADDED
@@ -0,0 +1,233 @@
1
+ import subprocess
2
+ import os
3
+ import sys
4
+ import json
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Dict, Any, List, Optional
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.markdown import Markdown
11
+
12
+ # ADK Imports
13
+ try:
14
+ from google.adk.agents import LlmAgent
15
+ from google.adk.models.lite_llm import LiteLlm
16
+ except ImportError:
17
+ # Fallback/Mock if ADK not present
18
+ LlmAgent = None
19
+ LiteLlm = None
20
+
21
+ from .tools.kit_tools import kit_index_tool, kit_symbols_tool, kit_file_tree_tool
22
+ from .llm import LLMClient # keep for fallback
23
+
24
+ try:
25
+ from ai_sprint.state import SprintStateManager
26
+ from ai_sprint.policy import PolicyEngine
27
+ from ai_sprint.trace import trace_specifications
28
+ except ImportError as e:
29
+ SprintStateManager = None
30
+ PolicyEngine = None
31
+
32
+ console = Console()
33
+
34
+ class CodeReviewer:
35
+ def __init__(self, project_root: Optional[str] = None):
36
+ if project_root:
37
+ self.project_root = Path(project_root).absolute()
38
+ else:
39
+ # Try to find project root by looking for .git
40
+ current = Path.cwd().absolute()
41
+ while current != current.parent:
42
+ if (current / ".git").exists():
43
+ self.project_root = current
44
+ break
45
+ current = current.parent
46
+ else:
47
+ self.project_root = Path.cwd().absolute()
48
+
49
+ self.console = Console()
50
+ self.use_agent = (LlmAgent is not None)
51
+
52
+ def _get_git_diff(self, target: str = "main") -> str:
53
+ """Get the git diff for the review context."""
54
+ try:
55
+ # If local changes (staged or unstaged)
56
+ res = subprocess.run(
57
+ ["git", "diff", target], capture_output=True, text=True
58
+ )
59
+ return res.stdout
60
+ except Exception:
61
+ return ""
62
+
63
+ def _get_sprint_context(self) -> Dict[str, Any]:
64
+ """Fetch active sprint context."""
65
+ if not SprintStateManager:
66
+ return {}
67
+
68
+ # Try to find active sprint
69
+ sprint_dir = self.project_root / ".sprint"
70
+ if not sprint_dir.exists():
71
+ return {}
72
+
73
+ # Simplified: find first active sprint
74
+ for item in sprint_dir.iterdir():
75
+ if item.is_dir() and item.name.startswith("0"):
76
+ # Check status
77
+ status_file = item / ".status"
78
+ if not status_file.exists() or status_file.read_text().strip() == "Active":
79
+ manager = SprintStateManager(item)
80
+ return manager.get_context_summary(None) # Get full summary
81
+ return {}
82
+
83
+ def _get_governance_rules(self) -> str:
84
+ """Read governance.yaml."""
85
+ gov_file = self.project_root / "governance.yaml"
86
+ if gov_file.exists():
87
+ return gov_file.read_text()
88
+ return "No governance.yaml found."
89
+
90
+ def review(self, pr_id: Optional[str] = None, local: bool = False):
91
+ """Perform a policy-grounded review."""
92
+ self.console.print("[cyan]Gathering Context Triad (Code, Intent, Law)...[/cyan]")
93
+
94
+ # 1. Code (The Change)
95
+ diff = self._get_git_diff("HEAD" if local else "main")
96
+ if not diff:
97
+ self.console.print("[yellow]No code changes detected.[/yellow]")
98
+ return
99
+
100
+ # 2. Intent (Sprint Context)
101
+ sprint_ctx = self._get_sprint_context() or {}
102
+ goal = sprint_ctx.get("goal", "Unknown Goal")
103
+ active_task_obj = sprint_ctx.get("active_task") or {}
104
+ active_task = active_task_obj.get("title", "Unknown Task")
105
+
106
+ # 3. Law (Governance)
107
+ rules = self._get_governance_rules()
108
+
109
+ # 4. The Verdict (Deterministic L1 Status)
110
+ verification_file = self.project_root / ".verification_results.json"
111
+ l1_verdict = "No deterministic L1 verification results found."
112
+ if verification_file.exists():
113
+ try:
114
+ l1_verdict = verification_file.read_text()
115
+ except Exception as e:
116
+ l1_verdict = f"Error reading L1 verification: {e}"
117
+
118
+ # Construct System Prompt (Intelligence)
119
+ system_prompt = f"""
120
+ You are the Governance Enforcer for OneCoder.
121
+
122
+ **THE RULES (governance.yaml)**:
123
+ {rules}
124
+
125
+ **THE INTENT (Sprint Context)**:
126
+ Goal: {goal}
127
+ Active Task: {active_task}
128
+
129
+ **THE VERDICT (Deterministic L1 Status)**:
130
+ {l1_verdict}
131
+
132
+ **YOUR MISSION**:
133
+ 1. Verify if the Code fulfills the Intent.
134
+ 2. PRIORITY: If there are L1 failures in THE VERDICT, analyze the specific error logs and provide a mitigation plan to fix them.
135
+ 3. Check for any other violations of The Rules (especially Banned Files, Architecture constraints).
136
+ 4. Assess code quality (Security, Performance).
137
+ 5. If the diff is ambiguous, USE YOUR TOOLS (kit_index, kit_symbols, etc.) to investigate the repository context.
138
+
139
+ **OUTPUT FORMAT**:
140
+ You must ALWAYS respond with a Strict JSON object:
141
+ {{
142
+ "pass": boolean,
143
+ "violations": ["list", "of", "strings"],
144
+ "feedback": "markdown string summarizing the review",
145
+ "mitigation_notes": "Technical notes for a coding agent to fix the violations. Be specific about files and changes needed."
146
+ }}
147
+ """
148
+ user_message = f"**THE CODE (Git Diff)**:\n{diff[:10000]} # Truncated"
149
+
150
+ self.console.print("[cyan]Consulting Intelligence (Agentic Reviewer)...[/cyan]")
151
+
152
+ if self.use_agent:
153
+ try:
154
+ # Use Agentic flow
155
+ asyncio.run(self._run_agent_review(system_prompt, user_message))
156
+ return
157
+ except Exception as e:
158
+ self.console.print(f"[yellow]Agentic review failed ({e}). Falling back to simple LLM.[/yellow]")
159
+
160
+ # Fallback to simple LLM
161
+ self._run_simple_review(system_prompt, user_message)
162
+
163
+ async def _run_agent_review(self, system_prompt: str, user_message: str):
164
+ """Run the review using LlmAgent with tools."""
165
+ model_name = os.getenv("ONECODER_MODEL", "openrouter/xiaomi/mimo-v2-flash:free")
166
+ model = LiteLlm(model_name)
167
+
168
+ agent = LlmAgent(
169
+ name="reviewer_agent",
170
+ model=model,
171
+ instruction=system_prompt,
172
+ tools=[kit_index_tool, kit_symbols_tool, kit_file_tree_tool],
173
+ output_key="final_response"
174
+ )
175
+
176
+ # Agent run
177
+ # We append user message to history or as prompt
178
+ # LlmAgent usually takes a user prompt
179
+ response = await agent.run(user_message)
180
+
181
+ # Parse output - Agent returns a string usually, hopefully JSON if instructed
182
+ try:
183
+ # Clean up markdown code blocks if present
184
+ clean_response = response.strip()
185
+ if clean_response.startswith("```json"):
186
+ clean_response = clean_response[7:]
187
+ if clean_response.endswith("```"):
188
+ clean_response = clean_response[:-3]
189
+
190
+ result = json.loads(clean_response)
191
+ self._render_result(result)
192
+ except json.JSONDecodeError:
193
+ self.console.print(f"[red]Agent output is not valid JSON:[/red] {response}")
194
+ sys.exit(1)
195
+
196
+ def _run_simple_review(self, system_prompt: str, user_message: str):
197
+ """Run legacy simple review."""
198
+ llm = LLMClient()
199
+ prompt = system_prompt + "\n\n" + user_message
200
+ result = llm.generate_json(prompt)
201
+ self._render_result(result)
202
+
203
+ def _render_result(self, result: Dict[str, Any]):
204
+ passed = result.get("pass", False)
205
+ color = "green" if passed else "red"
206
+
207
+ self.console.print(Panel(
208
+ Markdown(result.get("feedback", "")),
209
+ title=f"[{color}]Review Result: {'PASSED' if passed else 'FAILED'}[/{color}]",
210
+ border_style=color
211
+ ))
212
+
213
+ mitigation_notes = result.get("mitigation_notes")
214
+ if mitigation_notes:
215
+ self.console.print("\n[bold yellow]Mitigation Notes for Agent:[/bold yellow]")
216
+ self.console.print(mitigation_notes)
217
+
218
+ # Persist state for delegation
219
+ try:
220
+ state_dir = self.project_root / ".onecoder"
221
+ state_dir.mkdir(exist_ok=True)
222
+ state_file = state_dir / "review_state.json"
223
+ state_file.write_text(json.dumps(result, indent=2))
224
+ except Exception as e:
225
+ self.console.print(f"[dim]Failed to save review state: {e}[/dim]")
226
+
227
+ if not passed:
228
+ self.console.print("\n[bold red]Violations:[/bold red]")
229
+ for v in result.get("violations", []):
230
+ self.console.print(f" ❌ {v}")
231
+ sys.exit(1)
232
+ else:
233
+ self.console.print("\n[bold green]Policy Checks Passed.[/bold green]")
@@ -0,0 +1,209 @@
1
+ import logging
2
+ import os
3
+ from typing import List, Optional
4
+ from pathlib import Path
5
+
6
+ from ..models.delegation import DelegationSession
7
+ from ..worktree import WorktreeManager
8
+ from ..tmux import TmuxManager
9
+ from ..blackboard import BlackboardMemory
10
+ from ..backends.base import BaseBackend
11
+ from ..backends.local_tui import LocalTUIBackend
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class DelegationService:
16
+ """
17
+ Orchestrates delegated tasks using WorktreeManager and TmuxManager.
18
+ """
19
+
20
+ def __init__(self, blackboard: Optional[BlackboardMemory] = None):
21
+ self.worktree_mgr = WorktreeManager()
22
+ self.tmux_mgr = TmuxManager()
23
+ self.blackboard = blackboard or BlackboardMemory()
24
+ self.sessions_namespace = "delegation_sessions"
25
+ self.backends: Dict[str, BaseBackend] = {
26
+ "local-tui": LocalTUIBackend(self)
27
+ }
28
+
29
+ def create_session(self, task_id: str, backend: str = "local-tui", command: Optional[str] = None, external_id: Optional[str] = None) -> DelegationSession:
30
+ """Creates and persists a new delegation session."""
31
+ parent_branch = self.worktree_mgr.get_current_branch()
32
+ active_sprint_id = os.environ.get("ACTIVE_SPRINT_ID")
33
+
34
+ # Try to find spec_id from knowledge
35
+ spec_id = None
36
+ try:
37
+ from ..knowledge import ProjectKnowledge
38
+ pk = ProjectKnowledge()
39
+ knowledge = pk.get_l1_context()
40
+ if knowledge:
41
+ # Usually spec_id is in the goal or active task metadata in some implementations
42
+ # but for now we look for SPEC- prefix in active task title or goal
43
+ goal = knowledge.get("goal", "")
44
+ active_task = knowledge.get("active_task", {})
45
+
46
+ import re
47
+ spec_match = re.search(r"(SPEC-[A-Z0-9.-]+)", goal + " " + active_task.get("title", ""))
48
+ if spec_match:
49
+ spec_id = spec_match.group(1)
50
+ except Exception:
51
+ pass
52
+
53
+ session = DelegationSession(
54
+ task_id=task_id,
55
+ backend=backend,
56
+ command=command,
57
+ parent_branch=parent_branch,
58
+ sprint_id=active_sprint_id,
59
+ spec_id=spec_id,
60
+ external_id=external_id
61
+ )
62
+ self._save_session(session)
63
+ return session
64
+
65
+ def _save_session(self, session: DelegationSession):
66
+ """Persists session to blackboard memory."""
67
+ self.blackboard.set(
68
+ key=session.id,
69
+ value=session.model_dump(mode='json'),
70
+ scope="global",
71
+ namespace=self.sessions_namespace
72
+ )
73
+
74
+ def get_session(self, session_id: str) -> Optional[DelegationSession]:
75
+ """Retrieves a session from blackboard."""
76
+ data = self.blackboard.get(
77
+ key=session_id,
78
+ scope="global",
79
+ namespace=self.sessions_namespace
80
+ )
81
+ if data:
82
+ return DelegationSession(**data)
83
+ return None
84
+
85
+ def list_sessions(self) -> List[DelegationSession]:
86
+ """Lists all active delegation sessions."""
87
+ all_data = self.blackboard.get_all(scope="global", namespace=self.sessions_namespace)
88
+ return [DelegationSession(**data) for data in all_data.values()]
89
+
90
+ async def start_session(self, session: DelegationSession, context_metadata: Optional[str] = None) -> str:
91
+ """
92
+ Starts a delegation session using the appropriate backend.
93
+ """
94
+ backend = self.backends.get(session.backend)
95
+ if not backend:
96
+ raise ValueError(f"Unknown backend: {session.backend}")
97
+
98
+ return await backend.spawn(session)
99
+
100
+ def stop_session(self, session_id: str, cleanup: bool = True):
101
+ """Stops a session and optionally cleans up resources."""
102
+ session = self.get_session(session_id)
103
+ if not session:
104
+ return
105
+
106
+ if session.tmux_session:
107
+ self.tmux_mgr.kill_session(session.tmux_session)
108
+
109
+ if cleanup and session.worktree_path:
110
+ self.worktree_mgr.remove_worktree(session.id, delete_branch=True)
111
+
112
+ session.status = "stopped"
113
+ self._save_session(session)
114
+
115
+ def register_task_in_sprint(self, title: str, repo_root: Path) -> tuple[Optional[str], Optional[str]]:
116
+ """
117
+ Registers a new task in the active sprint's TODO.md and returns the generated ID.
118
+ Returns: (sprint_id, task_id)
119
+ """
120
+ try:
121
+ from ai_sprint.state import SprintStateManager
122
+ except ImportError:
123
+ logger.warning("ai_sprint SDK not available. Cannot register task.")
124
+ return None, None
125
+
126
+ sprint_dir = repo_root / ".sprint"
127
+ if not sprint_dir.exists():
128
+ return None, None
129
+
130
+ # 1. Detect Active Sprint
131
+ # Priority: ACTIVE_SPRINT_ID env > First Active Sprint found
132
+ active_sprint_id = os.environ.get("ACTIVE_SPRINT_ID")
133
+
134
+ if not active_sprint_id:
135
+ # Simple discovery: Find first dir with .status containing "Active" or missing .status
136
+ for item in sorted(sprint_dir.iterdir()):
137
+ if item.is_dir() and item.name[0].isdigit(): # basic check for sprint dir
138
+ status_file = item / ".status"
139
+ if not status_file.exists() or status_file.read_text().strip() == "Active":
140
+ active_sprint_id = item.name
141
+ break
142
+
143
+ if not active_sprint_id:
144
+ return None, None
145
+
146
+ target_sprint_dir = sprint_dir / active_sprint_id
147
+ todo_file = target_sprint_dir / "TODO.md"
148
+
149
+ if not todo_file.exists():
150
+ return None, None
151
+
152
+ # 2. Append to TODO.md
153
+ # specific format: "- [ ] {title}"
154
+ try:
155
+ with open(todo_file, "a") as f:
156
+ f.write(f"\n- [ ] {title}\n")
157
+
158
+ # 3. Sync and Retrieve ID
159
+ state_mgr = SprintStateManager(target_sprint_dir)
160
+ state_mgr.sync_from_files()
161
+ task_id = state_mgr.get_task_id_by_title(title)
162
+
163
+ return active_sprint_id, task_id
164
+
165
+ except Exception as e:
166
+ logger.error(f"Failed to register task in sprint: {e}")
167
+ return None, None
168
+
169
+ def _inject_context(self, wt_path: Path, session: DelegationSession, metadata: Optional[str] = None):
170
+ """Writes a BOOTSTRAP.md to the worktree to reduce agent TTU."""
171
+ bootstrap_path = wt_path / "BOOTSTRAP.md"
172
+ content = f"# Task: {session.task_id}\n\n"
173
+ content += f"## Branch Context\nParent Branch: `{session.parent_branch}`\n"
174
+ if session.sprint_id:
175
+ content += f"Active Sprint: `{session.sprint_id}`\n"
176
+ if session.spec_id:
177
+ content += f"Spec ID: `{session.spec_id}`\n"
178
+
179
+ content += f"\n## Instructions\n{session.command or 'No command provided.'}\n\n"
180
+ content += "## Repository Context\n"
181
+
182
+ # Try to gather automated knowledge
183
+ try:
184
+ from ..knowledge import ProjectKnowledge
185
+ pk = ProjectKnowledge()
186
+ knowledge = pk.get_rag_ready_output()
187
+ content += knowledge
188
+ except Exception:
189
+ content += "Automated context gathering failed.\n"
190
+
191
+ # Inject Mitigation Notes from recent review
192
+ try:
193
+ # Assume we are running from project root or can find it
194
+ review_state_path = Path(".onecoder/review_state.json")
195
+ if review_state_path.exists():
196
+ import json
197
+ state = json.loads(review_state_path.read_text())
198
+ mitigation_notes = state.get("mitigation_notes")
199
+ if mitigation_notes:
200
+ content += f"\n## Mitigation Notes (Recent Review)\n{mitigation_notes}\n"
201
+ except Exception as e:
202
+ logger.warning(f"Failed to inject review notes: {e}")
203
+
204
+ if metadata:
205
+ content += f"\n## Additional Metadata\n{metadata}\n"
206
+
207
+ with open(bootstrap_path, "w") as f:
208
+ f.write(content)
209
+ logger.info(f"Injected context at {bootstrap_path}")
@@ -0,0 +1,104 @@
1
+ import logging
2
+ import subprocess
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Any, Dict, Optional
5
+ from pathlib import Path
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class BaseValidationRule(ABC):
10
+ """Base class for all validation rules."""
11
+ @abstractmethod
12
+ def validate(self, context: Dict[str, Any]) -> bool:
13
+ pass
14
+
15
+ @property
16
+ @abstractmethod
17
+ def name(self) -> str:
18
+ pass
19
+
20
+ class FileExistsRule(BaseValidationRule):
21
+ """Checks if a file exists relative to a base path."""
22
+ def __init__(self, filepath: str):
23
+ self.filepath = filepath
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ return f"FileExists({self.filepath})"
28
+
29
+ def validate(self, context: Dict[str, Any]) -> bool:
30
+ base_path = context.get("worktree_path")
31
+ if not base_path:
32
+ return False
33
+ return (Path(base_path) / self.filepath).exists()
34
+
35
+ class CommandSuccessRule(BaseValidationRule):
36
+ """Checks if a command executes successfully (exit code 0)."""
37
+ def __init__(self, command: str):
38
+ self.command = command
39
+
40
+ @property
41
+ def name(self) -> str:
42
+ return f"CommandSuccess({self.command})"
43
+
44
+ def validate(self, context: Dict[str, Any]) -> bool:
45
+ base_path = context.get("worktree_path")
46
+ try:
47
+ result = subprocess.run(
48
+ self.command,
49
+ shell=True,
50
+ cwd=base_path,
51
+ capture_output=True,
52
+ check=False
53
+ )
54
+ return result.returncode == 0
55
+ except Exception:
56
+ return False
57
+
58
+ class ValidationReport:
59
+ """Structured report for validation results."""
60
+ def __init__(self, session_id: str, all_passed: bool, results: List[Dict[str, Any]]):
61
+ self.session_id = session_id
62
+ self.all_passed = all_passed
63
+ self.results = results
64
+
65
+ def to_dict(self) -> Dict[str, Any]:
66
+ return {
67
+ "session_id": self.session_id,
68
+ "all_passed": self.all_passed,
69
+ "results": self.results
70
+ }
71
+
72
+ class ValidationService:
73
+ """
74
+ ValidationService runs a set of rules against a delegation session.
75
+ """
76
+
77
+ def validate_session(self, session_context: Dict[str, Any], rules: List[BaseValidationRule]) -> ValidationReport:
78
+ """
79
+ Runs all rules and returns a structured ValidationReport.
80
+ """
81
+ session_id = session_context.get("session_id", "unknown")
82
+ results = []
83
+ all_passed = True
84
+
85
+ for rule in rules:
86
+ try:
87
+ passed = rule.validate(session_context)
88
+ results.append({
89
+ "rule": rule.name,
90
+ "passed": passed,
91
+ "error": None
92
+ })
93
+ if not passed:
94
+ all_passed = False
95
+ except Exception as e:
96
+ logger.error(f"Rule {rule.name} failed with error: {e}")
97
+ results.append({
98
+ "rule": rule.name,
99
+ "passed": False,
100
+ "error": str(e)
101
+ })
102
+ all_passed = False
103
+
104
+ return ValidationReport(session_id, all_passed, results)