velune-cli 0.9.0__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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Architecture Cognition Agent and Technical Debt Ledger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("velune.cognition.architecture")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ArchitectureDriftAlarm(Exception):
|
|
16
|
+
"""Exception raised when an architectural layering boundary rule is violated."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LCOMVisitor(ast.NodeVisitor):
|
|
22
|
+
"""AST visitor to calculate Lack of Cohesion of Methods (LCOM4/LCOM)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self.current_method: str | None = None
|
|
26
|
+
self.method_accessed_attributes: dict[str, set[str]] = {}
|
|
27
|
+
self.class_name: str | None = None
|
|
28
|
+
|
|
29
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
30
|
+
self.class_name = node.name
|
|
31
|
+
# Process the body of the class
|
|
32
|
+
for subnode in node.body:
|
|
33
|
+
if isinstance(subnode, ast.FunctionDef):
|
|
34
|
+
self.current_method = subnode.name
|
|
35
|
+
self.method_accessed_attributes[subnode.name] = set()
|
|
36
|
+
self.generic_visit(subnode)
|
|
37
|
+
self.current_method = None
|
|
38
|
+
# Do not recurse into nested classes to avoid confusion
|
|
39
|
+
|
|
40
|
+
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
41
|
+
# Check if attribute access is on 'self' (e.g. self.attr)
|
|
42
|
+
if self.current_method and isinstance(node.value, ast.Name) and node.value.id == "self":
|
|
43
|
+
self.method_accessed_attributes[self.current_method].add(node.attr)
|
|
44
|
+
self.generic_visit(node)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CognitiveDebtLedger:
|
|
48
|
+
"""Manages the persistent registry of technical debt, coupling, and modularity violations."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, ledger_path: str | Path | None = None) -> None:
|
|
51
|
+
if ledger_path is None:
|
|
52
|
+
self.ledger_path = Path(".velune") / "debt_ledger.json"
|
|
53
|
+
else:
|
|
54
|
+
self.ledger_path = Path(ledger_path)
|
|
55
|
+
|
|
56
|
+
self.ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
self._load_ledger()
|
|
58
|
+
|
|
59
|
+
def _load_ledger(self) -> None:
|
|
60
|
+
if self.ledger_path.exists():
|
|
61
|
+
try:
|
|
62
|
+
with open(self.ledger_path, encoding="utf-8") as f:
|
|
63
|
+
self.data = json.load(f)
|
|
64
|
+
except Exception:
|
|
65
|
+
self.data = {"debt_items": [], "total_severity": 0.0}
|
|
66
|
+
else:
|
|
67
|
+
self.data = {"debt_items": [], "total_severity": 0.0}
|
|
68
|
+
|
|
69
|
+
def _save_ledger(self) -> None:
|
|
70
|
+
try:
|
|
71
|
+
with open(self.ledger_path, "w", encoding="utf-8") as f:
|
|
72
|
+
json.dump(self.data, f, indent=2)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error("Failed to write to technical debt ledger: %s", e)
|
|
75
|
+
|
|
76
|
+
def add_debt_item(
|
|
77
|
+
self, file_path: str, category: str, description: str, severity: float
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Add or update an item in the debt ledger."""
|
|
80
|
+
# Check if duplicate exists
|
|
81
|
+
for item in self.data["debt_items"]:
|
|
82
|
+
if (
|
|
83
|
+
item["file_path"] == file_path
|
|
84
|
+
and item["category"] == category
|
|
85
|
+
and item["description"] == description
|
|
86
|
+
):
|
|
87
|
+
item["severity"] = severity
|
|
88
|
+
item["updated_at"] = (
|
|
89
|
+
os.path.getmtime(str(self.ledger_path)) if self.ledger_path.exists() else 0.0
|
|
90
|
+
)
|
|
91
|
+
break
|
|
92
|
+
else:
|
|
93
|
+
self.data["debt_items"].append(
|
|
94
|
+
{
|
|
95
|
+
"file_path": file_path,
|
|
96
|
+
"category": category,
|
|
97
|
+
"description": description,
|
|
98
|
+
"severity": severity,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Recompute total severity
|
|
103
|
+
self.data["total_severity"] = sum(item["severity"] for item in self.data["debt_items"])
|
|
104
|
+
self._save_ledger()
|
|
105
|
+
|
|
106
|
+
def clear_file_debt(self, file_path: str) -> None:
|
|
107
|
+
"""Clear all debt items registered for a specific file."""
|
|
108
|
+
self.data["debt_items"] = [
|
|
109
|
+
item for item in self.data["debt_items"] if item["file_path"] != file_path
|
|
110
|
+
]
|
|
111
|
+
self.data["total_severity"] = sum(item["severity"] for item in self.data["debt_items"])
|
|
112
|
+
self._save_ledger()
|
|
113
|
+
|
|
114
|
+
def get_items(self) -> list[dict[str, Any]]:
|
|
115
|
+
return self.data["debt_items"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ArchitectureCognitionAgent:
|
|
119
|
+
"""
|
|
120
|
+
Analyzes repository structure, calculates cohesion metrics (LCOM),
|
|
121
|
+
verifies architectural boundary coupling, and records technical debt.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self, workspace_root: str | None = None, ledger: CognitiveDebtLedger | None = None
|
|
126
|
+
) -> None:
|
|
127
|
+
self.workspace_root = workspace_root or os.getcwd()
|
|
128
|
+
self.ledger = ledger or CognitiveDebtLedger()
|
|
129
|
+
|
|
130
|
+
# Layering Rules: maps prefix -> list of disallowed module prefixes
|
|
131
|
+
self.layering_rules: list[tuple[str, list[str]]] = [
|
|
132
|
+
("velune/core", ["velune/execution", "velune/cognition", "velune/cli"]),
|
|
133
|
+
("velune/kernel", ["velune/execution", "velune/cognition", "velune/cli"]),
|
|
134
|
+
("velune/models", ["velune/providers", "velune/orchestration", "velune/cli"]),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
def calculate_lcom(self, code: str) -> dict[str, int]:
|
|
138
|
+
"""
|
|
139
|
+
Calculates Lack of Cohesion of Methods (LCOM) for each class in the code.
|
|
140
|
+
Returns a dictionary mapping class name to LCOM score.
|
|
141
|
+
"""
|
|
142
|
+
scores: dict[str, int] = {}
|
|
143
|
+
try:
|
|
144
|
+
tree = ast.parse(code)
|
|
145
|
+
except SyntaxError:
|
|
146
|
+
return scores
|
|
147
|
+
|
|
148
|
+
for node in ast.walk(tree):
|
|
149
|
+
if isinstance(node, ast.ClassDef):
|
|
150
|
+
visitor = LCOMVisitor()
|
|
151
|
+
visitor.visit(node)
|
|
152
|
+
|
|
153
|
+
methods = list(visitor.method_accessed_attributes.keys())
|
|
154
|
+
# Exclude constructor __init__ from scoring if preferred, or keep all.
|
|
155
|
+
# Let's count LCOM based on standard formula.
|
|
156
|
+
p = 0
|
|
157
|
+
q = 0
|
|
158
|
+
|
|
159
|
+
n = len(methods)
|
|
160
|
+
if n <= 1:
|
|
161
|
+
scores[node.name] = 0
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
for i in range(n):
|
|
165
|
+
for j in range(i + 1, n):
|
|
166
|
+
m1 = methods[i]
|
|
167
|
+
m2 = methods[j]
|
|
168
|
+
s1 = visitor.method_accessed_attributes[m1]
|
|
169
|
+
s2 = visitor.method_accessed_attributes[m2]
|
|
170
|
+
if s1.isdisjoint(s2):
|
|
171
|
+
p += 1
|
|
172
|
+
else:
|
|
173
|
+
q += 1
|
|
174
|
+
|
|
175
|
+
lcom = p - q if p > q else 0
|
|
176
|
+
scores[node.name] = lcom
|
|
177
|
+
|
|
178
|
+
return scores
|
|
179
|
+
|
|
180
|
+
def calculate_coupling_ratio(self, directory: str) -> float:
|
|
181
|
+
"""
|
|
182
|
+
Calculates coupling ratio recursively over a directory's python files:
|
|
183
|
+
external_project_imports / (internal_project_imports + external_project_imports).
|
|
184
|
+
Project imports are identified by a 'velune' module prefix.
|
|
185
|
+
"""
|
|
186
|
+
abs_dir = os.path.abspath(directory)
|
|
187
|
+
parts = Path(abs_dir).parts
|
|
188
|
+
if "velune" in parts:
|
|
189
|
+
idx = parts.index("velune")
|
|
190
|
+
rel_parts = parts[idx:]
|
|
191
|
+
prefix = ".".join(rel_parts)
|
|
192
|
+
else:
|
|
193
|
+
prefix = "velune." + os.path.basename(abs_dir)
|
|
194
|
+
|
|
195
|
+
internal_count = 0
|
|
196
|
+
external_count = 0
|
|
197
|
+
|
|
198
|
+
for root, _, files in os.walk(directory):
|
|
199
|
+
for file in files:
|
|
200
|
+
if file.endswith(".py"):
|
|
201
|
+
file_path = os.path.join(root, file)
|
|
202
|
+
try:
|
|
203
|
+
with open(file_path, encoding="utf-8", errors="ignore") as f:
|
|
204
|
+
content = f.read()
|
|
205
|
+
tree = ast.parse(content)
|
|
206
|
+
for node in ast.walk(tree):
|
|
207
|
+
imported_mods = []
|
|
208
|
+
if isinstance(node, ast.Import):
|
|
209
|
+
for name in node.names:
|
|
210
|
+
imported_mods.append(name.name)
|
|
211
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
212
|
+
imported_mods.append(node.module)
|
|
213
|
+
|
|
214
|
+
for imported_mod in imported_mods:
|
|
215
|
+
if imported_mod == "velune" or imported_mod.startswith("velune."):
|
|
216
|
+
if imported_mod.startswith(prefix):
|
|
217
|
+
internal_count += 1
|
|
218
|
+
else:
|
|
219
|
+
external_count += 1
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
total = internal_count + external_count
|
|
224
|
+
if total == 0:
|
|
225
|
+
return 0.0
|
|
226
|
+
return round(external_count / total, 3)
|
|
227
|
+
|
|
228
|
+
def calculate_shi(self, directory: str) -> float:
|
|
229
|
+
"""
|
|
230
|
+
Calculates the Subsystem Health Index (SHI) for the given directory.
|
|
231
|
+
SHI(M) = max(0.0, min(1.0, 1.0 * Cohesion - 0.5 * Coupling - 0.2 * DebtPenalty))
|
|
232
|
+
"""
|
|
233
|
+
# Calculate Average LCOM
|
|
234
|
+
lcom_scores = []
|
|
235
|
+
for root, _, files in os.walk(directory):
|
|
236
|
+
for file in files:
|
|
237
|
+
if file.endswith(".py"):
|
|
238
|
+
file_path = os.path.join(root, file)
|
|
239
|
+
try:
|
|
240
|
+
with open(file_path, encoding="utf-8", errors="ignore") as f:
|
|
241
|
+
content = f.read()
|
|
242
|
+
scores = self.calculate_lcom(content)
|
|
243
|
+
lcom_scores.extend(scores.values())
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
if lcom_scores:
|
|
248
|
+
avg_lcom = sum(lcom_scores) / len(lcom_scores)
|
|
249
|
+
cohesion = 1.0 - min(avg_lcom / 10.0, 1.0)
|
|
250
|
+
else:
|
|
251
|
+
cohesion = 1.0
|
|
252
|
+
|
|
253
|
+
coupling = self.calculate_coupling_ratio(directory)
|
|
254
|
+
|
|
255
|
+
# Calculate Debt Penalty
|
|
256
|
+
norm_dir = os.path.abspath(directory)
|
|
257
|
+
sum_severity = 0.0
|
|
258
|
+
for item in self.ledger.get_items():
|
|
259
|
+
item_path = os.path.abspath(item["file_path"])
|
|
260
|
+
if item_path.startswith(norm_dir):
|
|
261
|
+
sum_severity += item.get("severity", 0.0)
|
|
262
|
+
|
|
263
|
+
debt_penalty = min(sum_severity * 0.1, 1.0)
|
|
264
|
+
|
|
265
|
+
shi = 1.0 * cohesion - 0.5 * coupling - 0.2 * debt_penalty
|
|
266
|
+
return max(0.0, min(1.0, round(shi, 3)))
|
|
267
|
+
|
|
268
|
+
def propose_refactoring(self, directory: str) -> str | None:
|
|
269
|
+
"""Generates proactive refactoring suggestions in markdown when SHI < 0.60."""
|
|
270
|
+
shi = self.calculate_shi(directory)
|
|
271
|
+
if shi >= 0.60:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
proposal = f"# Proactive Refactoring Proposal: `{os.path.basename(directory)}`\n\n"
|
|
275
|
+
proposal += f"The subsystem health index (**SHI: {shi:.2f}**) has fallen below the acceptable threshold (0.60).\n"
|
|
276
|
+
proposal += "This module is currently classified as an **Architectural Sink** and requires immediate refactoring.\n\n"
|
|
277
|
+
|
|
278
|
+
proposal += "## Diagnostic Summary\n"
|
|
279
|
+
coupling = self.calculate_coupling_ratio(directory)
|
|
280
|
+
proposal += f"- **Coupling Ratio**: {coupling:.2f} (Target: < 0.3)\n"
|
|
281
|
+
|
|
282
|
+
# Cohesion (LCOM) diagnostic
|
|
283
|
+
lcom_scores = []
|
|
284
|
+
for root, _, files in os.walk(directory):
|
|
285
|
+
for file in files:
|
|
286
|
+
if file.endswith(".py"):
|
|
287
|
+
file_path = os.path.join(root, file)
|
|
288
|
+
try:
|
|
289
|
+
with open(file_path, encoding="utf-8", errors="ignore") as f:
|
|
290
|
+
scores = self.calculate_lcom(f.read())
|
|
291
|
+
for cls, score in scores.items():
|
|
292
|
+
lcom_scores.append((cls, score, file))
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
if lcom_scores:
|
|
296
|
+
avg_lcom = sum(s[1] for s in lcom_scores) / len(lcom_scores)
|
|
297
|
+
cohesion = 1.0 - min(avg_lcom / 10.0, 1.0)
|
|
298
|
+
proposal += f"- **Cohesion Score**: {cohesion:.2f} (Average LCOM: {avg_lcom:.1f})\n"
|
|
299
|
+
high_lcom = [s for s in lcom_scores if s[1] > 5]
|
|
300
|
+
if high_lcom:
|
|
301
|
+
proposal += "\n### High Lack of Cohesion of Methods (LCOM) Classes:\n"
|
|
302
|
+
for cls, score, file in high_lcom:
|
|
303
|
+
proposal += f" - Class `{cls}` in `{file}` has LCOM score of {score}.\n"
|
|
304
|
+
else:
|
|
305
|
+
proposal += "- **Cohesion Score**: 1.00 (No classes found)\n"
|
|
306
|
+
|
|
307
|
+
# Debt diagnostics
|
|
308
|
+
norm_dir = os.path.abspath(directory)
|
|
309
|
+
debt_items = []
|
|
310
|
+
for item in self.ledger.get_items():
|
|
311
|
+
item_path = os.path.abspath(item["file_path"])
|
|
312
|
+
if item_path.startswith(norm_dir):
|
|
313
|
+
debt_items.append(item)
|
|
314
|
+
if debt_items:
|
|
315
|
+
proposal += "\n## Outstanding Technical Debt Items\n"
|
|
316
|
+
for item in debt_items:
|
|
317
|
+
proposal += f"- **[{item['category'].upper()}]** `{os.path.basename(item['file_path'])}`: {item['description']} (Severity: {item['severity']})\n"
|
|
318
|
+
|
|
319
|
+
proposal += "\n## Action Plan\n"
|
|
320
|
+
proposal += "1. **Split High LCOM Classes**: Refactor classes with high cohesion issues into smaller, more focused modules.\n"
|
|
321
|
+
proposal += "2. **Reduce Module Coupling**: Minimize external dependencies and enforce clean interface boundaries.\n"
|
|
322
|
+
proposal += "3. **Clear Technical Debt**: Address the boundary layering violations and resolve critical warnings immediately.\n"
|
|
323
|
+
return proposal
|
|
324
|
+
|
|
325
|
+
def verify_boundaries(
|
|
326
|
+
self, proposed_code: str, file_path: str, raise_on_violation: bool = False
|
|
327
|
+
) -> list[str]:
|
|
328
|
+
"""
|
|
329
|
+
Checks proposed Python code for clean architecture layering and modular boundary violations.
|
|
330
|
+
Returns a list of violations found.
|
|
331
|
+
Raises ArchitectureDriftAlarm if raise_on_violation is True and violations exist.
|
|
332
|
+
"""
|
|
333
|
+
violations = []
|
|
334
|
+
# Standardize path slashes for matching
|
|
335
|
+
norm_path = file_path.replace("\\", "/")
|
|
336
|
+
|
|
337
|
+
# Identify if target file has active layering rules
|
|
338
|
+
disallowed_prefixes: list[str] = []
|
|
339
|
+
for prefix, rules in self.layering_rules:
|
|
340
|
+
if norm_path.startswith(prefix):
|
|
341
|
+
disallowed_prefixes.extend(rules)
|
|
342
|
+
|
|
343
|
+
if not disallowed_prefixes:
|
|
344
|
+
return violations
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
tree = ast.parse(proposed_code)
|
|
348
|
+
except SyntaxError:
|
|
349
|
+
return violations
|
|
350
|
+
|
|
351
|
+
for node in ast.walk(tree):
|
|
352
|
+
imported_mod: str | None = None
|
|
353
|
+
if isinstance(node, ast.Import):
|
|
354
|
+
for name in node.names:
|
|
355
|
+
imported_mod = name.name
|
|
356
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
357
|
+
imported_mod = node.module
|
|
358
|
+
|
|
359
|
+
if imported_mod:
|
|
360
|
+
norm_import = imported_mod.replace(".", "/")
|
|
361
|
+
for disallowed in disallowed_prefixes:
|
|
362
|
+
if norm_import.startswith(disallowed):
|
|
363
|
+
violations.append(
|
|
364
|
+
f"Layering violation: '{file_path}' imports '{imported_mod}' which is banned."
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if raise_on_violation and violations:
|
|
368
|
+
raise ArchitectureDriftAlarm("\n".join(violations))
|
|
369
|
+
|
|
370
|
+
return violations
|
|
371
|
+
|
|
372
|
+
def audit_architecture(self, file_path: str, code: str) -> dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Performs full cohesion and architectural boundary audit.
|
|
375
|
+
Records violations and high LCOM classes in the Technical Debt Ledger.
|
|
376
|
+
"""
|
|
377
|
+
self.ledger.clear_file_debt(file_path)
|
|
378
|
+
|
|
379
|
+
lcom_scores = self.calculate_lcom(code)
|
|
380
|
+
boundary_violations = self.verify_boundaries(code, file_path)
|
|
381
|
+
|
|
382
|
+
# Log LCOM high scores as debt (LCOM > 5 is considered high)
|
|
383
|
+
for class_name, score in lcom_scores.items():
|
|
384
|
+
if score > 5:
|
|
385
|
+
self.ledger.add_debt_item(
|
|
386
|
+
file_path=file_path,
|
|
387
|
+
category="cohesion",
|
|
388
|
+
description=f"Class '{class_name}' has high LCOM score ({score}). Consider splitting its responsibilities.",
|
|
389
|
+
severity=round(score * 0.1, 2),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Log boundary violations as high-severity debt
|
|
393
|
+
for violation in boundary_violations:
|
|
394
|
+
self.ledger.add_debt_item(
|
|
395
|
+
file_path=file_path,
|
|
396
|
+
category="layering",
|
|
397
|
+
description=violation,
|
|
398
|
+
severity=1.5,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
dir_path = os.path.dirname(os.path.abspath(file_path))
|
|
402
|
+
coupling = self.calculate_coupling_ratio(dir_path)
|
|
403
|
+
shi = self.calculate_shi(dir_path)
|
|
404
|
+
proposal = self.propose_refactoring(dir_path)
|
|
405
|
+
|
|
406
|
+
passed = len(boundary_violations) == 0
|
|
407
|
+
return {
|
|
408
|
+
"passed": passed,
|
|
409
|
+
"lcom_scores": lcom_scores,
|
|
410
|
+
"violations": boundary_violations,
|
|
411
|
+
"total_debt": len(boundary_violations) + sum(1 for s in lcom_scores.values() if s > 5),
|
|
412
|
+
"coupling_ratio": coupling,
|
|
413
|
+
"shi": shi,
|
|
414
|
+
"refactoring_proposal": proposal,
|
|
415
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Council execution budget — hard time and cycle guards for agent deliberation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from velune.cli.modes import SessionMode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CouncilExecutionBudget:
|
|
14
|
+
"""Hard budget applied to every council deliberation run.
|
|
15
|
+
|
|
16
|
+
All ``*_seconds`` values are wall-clock limits. When passed to
|
|
17
|
+
:meth:`CouncilOrchestrator.execute_task`, these values override the
|
|
18
|
+
instance-level ``max_wall_time_seconds`` and inject per-agent
|
|
19
|
+
``asyncio.wait_for`` guards that prevent hangs from unresponsive models.
|
|
20
|
+
|
|
21
|
+
Review cycle cap
|
|
22
|
+
----------------
|
|
23
|
+
``max_review_cycles`` is the binding cap on the debate loop. Even when
|
|
24
|
+
:func:`calculate_max_debate_turns` returns a higher number (e.g. 4 for a
|
|
25
|
+
security failure), the debate stops after ``max_review_cycles`` turns so a
|
|
26
|
+
single stubborn critic cannot loop a session indefinitely.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
max_wall_time_seconds: int = 120
|
|
30
|
+
max_tokens_per_agent: int = 4096
|
|
31
|
+
max_review_cycles: int = 2
|
|
32
|
+
planner_timeout_seconds: int = 30
|
|
33
|
+
coder_timeout_seconds: int = 60
|
|
34
|
+
reviewer_timeout_seconds: int = 30
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_session_mode(cls, mode: SessionMode) -> CouncilExecutionBudget:
|
|
38
|
+
"""Return a budget calibrated to the given session mode.
|
|
39
|
+
|
|
40
|
+
* ``OPTIMUS`` — tightest budgets; speed over depth, 1 review cycle.
|
|
41
|
+
* ``NORMAL`` — balanced defaults (2 review cycles, 120s wall).
|
|
42
|
+
* ``GODLY`` — relaxed but still bounded; depth over speed, 3 cycles.
|
|
43
|
+
"""
|
|
44
|
+
from velune.cli.modes import SessionMode
|
|
45
|
+
|
|
46
|
+
if mode == SessionMode.OPTIMUS:
|
|
47
|
+
return cls(
|
|
48
|
+
max_wall_time_seconds=60,
|
|
49
|
+
max_tokens_per_agent=2048,
|
|
50
|
+
max_review_cycles=1,
|
|
51
|
+
planner_timeout_seconds=15,
|
|
52
|
+
coder_timeout_seconds=30,
|
|
53
|
+
reviewer_timeout_seconds=15,
|
|
54
|
+
)
|
|
55
|
+
if mode == SessionMode.GODLY:
|
|
56
|
+
return cls(
|
|
57
|
+
max_wall_time_seconds=300,
|
|
58
|
+
max_tokens_per_agent=8192,
|
|
59
|
+
max_review_cycles=3,
|
|
60
|
+
planner_timeout_seconds=60,
|
|
61
|
+
coder_timeout_seconds=120,
|
|
62
|
+
reviewer_timeout_seconds=60,
|
|
63
|
+
)
|
|
64
|
+
# SessionMode.NORMAL — default values
|
|
65
|
+
return cls()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Reasoning Council agents package."""
|
|
2
|
+
|
|
3
|
+
from velune.cognition.council.base import BaseCouncilAgent
|
|
4
|
+
from velune.cognition.council.challenger import ChallengerAgent
|
|
5
|
+
from velune.cognition.council.coder import CoderAgent
|
|
6
|
+
from velune.cognition.council.critic_agent import CriticAgent
|
|
7
|
+
from velune.cognition.council.critic_configs import (
|
|
8
|
+
MAINTAINABILITY_CONFIG,
|
|
9
|
+
PERFORMANCE_CONFIG,
|
|
10
|
+
SCALABILITY_CONFIG,
|
|
11
|
+
SECURITY_CONFIG,
|
|
12
|
+
CriticConfig,
|
|
13
|
+
)
|
|
14
|
+
from velune.cognition.council.critics import (
|
|
15
|
+
MaintainabilityCritic,
|
|
16
|
+
PerformanceCritic,
|
|
17
|
+
ScalabilityCritic,
|
|
18
|
+
SecurityCritic,
|
|
19
|
+
)
|
|
20
|
+
from velune.cognition.council.debate import (
|
|
21
|
+
DebateConfig,
|
|
22
|
+
calculate_max_debate_turns,
|
|
23
|
+
)
|
|
24
|
+
from velune.cognition.council.planner import PlannerAgent
|
|
25
|
+
from velune.cognition.council.reviewer import ReviewerAgent
|
|
26
|
+
from velune.cognition.council.synthesizer import SynthesizerAgent
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"BaseCouncilAgent",
|
|
30
|
+
"PlannerAgent",
|
|
31
|
+
"CoderAgent",
|
|
32
|
+
"ReviewerAgent",
|
|
33
|
+
"ChallengerAgent",
|
|
34
|
+
"SynthesizerAgent",
|
|
35
|
+
"CriticAgent",
|
|
36
|
+
"CriticConfig",
|
|
37
|
+
"SCALABILITY_CONFIG",
|
|
38
|
+
"SECURITY_CONFIG",
|
|
39
|
+
"PERFORMANCE_CONFIG",
|
|
40
|
+
"MAINTAINABILITY_CONFIG",
|
|
41
|
+
"ScalabilityCritic",
|
|
42
|
+
"SecurityCritic",
|
|
43
|
+
"PerformanceCritic",
|
|
44
|
+
"MaintainabilityCritic",
|
|
45
|
+
"DebateConfig",
|
|
46
|
+
"calculate_max_debate_turns",
|
|
47
|
+
]
|