gdmcode 0.1.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.
- gdmcode-0.1.0.dist-info/METADATA +240 -0
- gdmcode-0.1.0.dist-info/RECORD +131 -0
- gdmcode-0.1.0.dist-info/WHEEL +4 -0
- gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
- src/__init__.py +1 -0
- src/_internal/__init__.py +0 -0
- src/_internal/constants.py +244 -0
- src/_internal/domain_skills.py +339 -0
- src/agent/__init__.py +0 -0
- src/agent/commit_classifier.py +91 -0
- src/agent/context_budget.py +391 -0
- src/agent/daemon.py +681 -0
- src/agent/dag_validator.py +153 -0
- src/agent/debug_loop.py +473 -0
- src/agent/impact_analyzer.py +149 -0
- src/agent/impact_graph.py +117 -0
- src/agent/loop.py +1410 -0
- src/agent/orchestrator.py +141 -0
- src/agent/regression_guard.py +251 -0
- src/agent/review_gate.py +648 -0
- src/agent/risk_scorer.py +169 -0
- src/agent/self_healing.py +145 -0
- src/agent/smart_test_selector.py +89 -0
- src/agent/system_prompt.py +226 -0
- src/agent/task_tracker.py +320 -0
- src/agent/test_validator.py +210 -0
- src/agent/tool_orchestrator.py +402 -0
- src/agent/transcript.py +230 -0
- src/agent/verification_loop.py +133 -0
- src/agent/work_director.py +136 -0
- src/agent/worktree_manager.py +53 -0
- src/artifacts/__init__.py +16 -0
- src/artifacts/artifact_store.py +456 -0
- src/artifacts/verification_graph.py +75 -0
- src/auth.py +411 -0
- src/cli.py +1290 -0
- src/commands.py +1398 -0
- src/config.py +762 -0
- src/cost_tracker.py +348 -0
- src/db/__init__.py +4 -0
- src/db/migrations.py +337 -0
- src/enterprise/__init__.py +3 -0
- src/enterprise/audit_log.py +182 -0
- src/enterprise/identity.py +90 -0
- src/enterprise/rbac.py +100 -0
- src/enterprise/team_config.py +125 -0
- src/enterprise/usage_analytics.py +261 -0
- src/exceptions.py +207 -0
- src/git_workflow.py +651 -0
- src/integrations/__init__.py +6 -0
- src/integrations/github_actions.py +106 -0
- src/integrations/mcp_server.py +333 -0
- src/integrations/sentry_integration.py +100 -0
- src/integrations/sentry_server.py +82 -0
- src/integrations/webhook_security.py +19 -0
- src/main.py +27 -0
- src/memory/__init__.py +0 -0
- src/memory/code_index.py +376 -0
- src/memory/compressor.py +378 -0
- src/memory/context_memory.py +135 -0
- src/memory/continuous_memory.py +234 -0
- src/memory/conventions.py +495 -0
- src/memory/db.py +1119 -0
- src/memory/document_index.py +205 -0
- src/memory/file_cache.py +128 -0
- src/memory/project_scanner.py +178 -0
- src/memory/session_store.py +201 -0
- src/models/__init__.py +0 -0
- src/models/client.py +715 -0
- src/models/definitions.py +459 -0
- src/models/router.py +418 -0
- src/models/schemas.py +389 -0
- src/permissions.py +294 -0
- src/remote/__init__.py +5 -0
- src/remote/command_filter.py +33 -0
- src/remote/models.py +31 -0
- src/remote/permission_handler.py +79 -0
- src/remote/phone_ui.py +48 -0
- src/remote/protocol.py +59 -0
- src/remote/qr.py +65 -0
- src/remote/server.py +586 -0
- src/remote/token_manager.py +61 -0
- src/remote/tunnel.py +212 -0
- src/repl.py +475 -0
- src/runtime/__init__.py +1 -0
- src/runtime/branch_farm.py +372 -0
- src/runtime/replay.py +351 -0
- src/sandbox/__init__.py +2 -0
- src/sandbox/hermetic.py +214 -0
- src/sandbox/policy.py +44 -0
- src/sdk/__init__.py +3 -0
- src/sdk/plugin_base.py +39 -0
- src/sdk/plugin_host.py +100 -0
- src/sdk/plugin_loader.py +101 -0
- src/security.py +409 -0
- src/server/__init__.py +7 -0
- src/server/bridge.py +427 -0
- src/server/bridge_cli.py +103 -0
- src/server/bridge_client.py +170 -0
- src/server/protocol_version.py +103 -0
- src/session/__init__.py +10 -0
- src/session/event_fanout.py +46 -0
- src/session/input_broker.py +38 -0
- src/session/permission_bridge.py +100 -0
- src/tools/__init__.py +160 -0
- src/tools/_atomic.py +72 -0
- src/tools/agent_tools.py +423 -0
- src/tools/ask_user_tool.py +83 -0
- src/tools/bash_tool.py +384 -0
- src/tools/browser_tool.py +352 -0
- src/tools/browser_tools.py +179 -0
- src/tools/dep_tools.py +210 -0
- src/tools/document_reader.py +167 -0
- src/tools/document_tool.py +240 -0
- src/tools/document_writer.py +171 -0
- src/tools/impact_tools.py +240 -0
- src/tools/playwright_tool.py +172 -0
- src/tools/quality_tools.py +366 -0
- src/tools/read_tools.py +318 -0
- src/tools/result_cache.py +157 -0
- src/tools/search_tools.py +310 -0
- src/tools/shell_tools.py +311 -0
- src/tools/write_tools.py +337 -0
- src/voice/__init__.py +25 -0
- src/voice/audio_capture.py +92 -0
- src/voice/audio_playback.py +68 -0
- src/voice/errors.py +14 -0
- src/voice/models.py +35 -0
- src/voice/providers.py +143 -0
- src/voice/vad.py +55 -0
- src/voice/voice_loop.py +156 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Pre-edit impact gate: score risk before applying file edits."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
__all__ = ["ImpactAnalyzer", "ImpactGateError", "ImpactGateResult"]
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImpactGateError(Exception):
|
|
14
|
+
"""Raised when a high-risk edit is blocked (enforce=True)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, result: "ImpactGateResult | None" = None) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.result = result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ImpactGateResult:
|
|
23
|
+
allowed: bool
|
|
24
|
+
risk_score: float # 0.0 (safe) - 1.0 (critical)
|
|
25
|
+
callers_count: int
|
|
26
|
+
is_public_api: bool
|
|
27
|
+
semver_recommendation: str # "patch" | "minor" | "major"
|
|
28
|
+
reason: str # human-readable explanation
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ImpactAnalyzer:
|
|
32
|
+
"""Scores the risk of editing a file and optionally blocks high-risk edits."""
|
|
33
|
+
|
|
34
|
+
# Risk thresholds
|
|
35
|
+
WARN_THRESHOLD = 0.5 # log warning
|
|
36
|
+
BLOCK_THRESHOLD = 0.85 # block edit (when enforce=True)
|
|
37
|
+
|
|
38
|
+
def __init__(self, project_root: str | Path = ".", enforce: bool = False) -> None:
|
|
39
|
+
self._root = Path(project_root)
|
|
40
|
+
self._enforce = enforce # if True, raise on high-risk; if False, warn only
|
|
41
|
+
|
|
42
|
+
def gate(self, file_path: str, change_description: str = "") -> ImpactGateResult:
|
|
43
|
+
"""Score impact for file_path. Raise ImpactGateError if blocked."""
|
|
44
|
+
callers_count, is_public = self._get_impact_report(file_path, change_description)
|
|
45
|
+
risk_score = self._compute_risk(callers_count, is_public, file_path)
|
|
46
|
+
allowed = risk_score < self.BLOCK_THRESHOLD
|
|
47
|
+
semver = self._semver(callers_count, is_public)
|
|
48
|
+
|
|
49
|
+
if not allowed:
|
|
50
|
+
reason = (
|
|
51
|
+
f"Blocked: high-risk edit to '{file_path}' "
|
|
52
|
+
f"(score={risk_score:.2f}, callers={callers_count}, "
|
|
53
|
+
f"public_api={is_public})"
|
|
54
|
+
)
|
|
55
|
+
elif risk_score >= self.WARN_THRESHOLD:
|
|
56
|
+
reason = (
|
|
57
|
+
f"Warning: moderate risk for '{file_path}' "
|
|
58
|
+
f"(score={risk_score:.2f}, callers={callers_count})"
|
|
59
|
+
)
|
|
60
|
+
log.warning(reason)
|
|
61
|
+
else:
|
|
62
|
+
reason = f"Allowed: low-risk edit to '{file_path}' (score={risk_score:.2f})"
|
|
63
|
+
|
|
64
|
+
result = ImpactGateResult(
|
|
65
|
+
allowed=allowed,
|
|
66
|
+
risk_score=risk_score,
|
|
67
|
+
callers_count=callers_count,
|
|
68
|
+
is_public_api=is_public,
|
|
69
|
+
semver_recommendation=semver,
|
|
70
|
+
reason=reason,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not allowed and self._enforce:
|
|
74
|
+
raise ImpactGateError(reason, result)
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
def _compute_risk(self, callers: int, is_public: bool, path: str) -> float:
|
|
79
|
+
"""Heuristic: public API + many callers = high risk."""
|
|
80
|
+
score = 0.0
|
|
81
|
+
|
|
82
|
+
if is_public:
|
|
83
|
+
score += 0.5
|
|
84
|
+
|
|
85
|
+
if callers > 10:
|
|
86
|
+
score += 0.45
|
|
87
|
+
elif callers > 5:
|
|
88
|
+
score += 0.35
|
|
89
|
+
elif callers > 0:
|
|
90
|
+
score += 0.2
|
|
91
|
+
|
|
92
|
+
# Private files (underscore-prefixed) carry slightly less risk.
|
|
93
|
+
if Path(path).stem.startswith("_"):
|
|
94
|
+
score -= 0.1
|
|
95
|
+
|
|
96
|
+
return max(0.0, min(1.0, score))
|
|
97
|
+
|
|
98
|
+
def _get_impact_report(self, file_path: str, change_description: str) -> tuple[int, bool]:
|
|
99
|
+
"""Call impact_tools.ImpactTool or fall back to heuristic if unavailable."""
|
|
100
|
+
module_name = Path(file_path).stem
|
|
101
|
+
try:
|
|
102
|
+
from src.tools.impact_tools import ImpactAnalysisTool # noqa: PLC0415
|
|
103
|
+
tool = ImpactAnalysisTool()
|
|
104
|
+
callers = tool._find_callers(module_name, self._root, include_tests=True)
|
|
105
|
+
is_public = tool._is_public_api(module_name, self._root)
|
|
106
|
+
return len(callers), is_public
|
|
107
|
+
except Exception: # noqa: BLE001
|
|
108
|
+
log.debug("ImpactAnalysisTool unavailable for %r; using heuristic", file_path)
|
|
109
|
+
return self._heuristic_callers(file_path), self._heuristic_public(file_path)
|
|
110
|
+
|
|
111
|
+
def _heuristic_callers(self, file_path: str) -> int:
|
|
112
|
+
"""Count Python files that reference this module by stem name."""
|
|
113
|
+
stem = Path(file_path).stem
|
|
114
|
+
count = 0
|
|
115
|
+
try:
|
|
116
|
+
target = Path(file_path).resolve()
|
|
117
|
+
for py_file in self._root.rglob("*.py"):
|
|
118
|
+
if py_file.resolve() == target:
|
|
119
|
+
continue
|
|
120
|
+
try:
|
|
121
|
+
if stem in py_file.read_text(errors="ignore"):
|
|
122
|
+
count += 1
|
|
123
|
+
except OSError:
|
|
124
|
+
continue
|
|
125
|
+
except Exception: # noqa: BLE001
|
|
126
|
+
pass
|
|
127
|
+
return count
|
|
128
|
+
|
|
129
|
+
def _heuristic_public(self, file_path: str) -> bool:
|
|
130
|
+
"""Return True if the file's stem appears in any __init__.py."""
|
|
131
|
+
stem = Path(file_path).stem
|
|
132
|
+
try:
|
|
133
|
+
for init in self._root.rglob("__init__.py"):
|
|
134
|
+
try:
|
|
135
|
+
if stem in init.read_text(errors="ignore"):
|
|
136
|
+
return True
|
|
137
|
+
except OSError:
|
|
138
|
+
continue
|
|
139
|
+
except Exception: # noqa: BLE001
|
|
140
|
+
pass
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _semver(callers: int, is_public: bool) -> str:
|
|
145
|
+
if is_public:
|
|
146
|
+
return "major"
|
|
147
|
+
if callers > 5:
|
|
148
|
+
return "minor"
|
|
149
|
+
return "patch"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Impact Graph — static dependency analysis for Python codebases.
|
|
3
|
+
|
|
4
|
+
Builds a file-level import dependency graph, then given a set of changed files,
|
|
5
|
+
computes the transitive impact set (all files that may be affected by the change).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import ast
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class ImpactGraph:
|
|
17
|
+
"""
|
|
18
|
+
Directed graph where edge A→B means "A imports from B" (B impacts A).
|
|
19
|
+
To find what is impacted by changing file X: find all nodes that transitively
|
|
20
|
+
import X (reverse edges).
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._deps: dict[str, set[str]] = {} # file → set of files it imports
|
|
24
|
+
self._reverse: dict[str, set[str]] = {} # file → set of files that import it
|
|
25
|
+
|
|
26
|
+
def add_file(self, file_path: str, imports: list[str]) -> None:
|
|
27
|
+
self._deps[file_path] = set(imports)
|
|
28
|
+
for imp in imports:
|
|
29
|
+
self._reverse.setdefault(imp, set()).add(file_path)
|
|
30
|
+
|
|
31
|
+
def get_dependencies(self, file_path: str) -> set[str]:
|
|
32
|
+
"""Files that file_path depends on (imports)."""
|
|
33
|
+
return set(self._deps.get(file_path, set()))
|
|
34
|
+
|
|
35
|
+
def get_dependents(self, file_path: str) -> set[str]:
|
|
36
|
+
"""Files that import file_path (directly impacted by changes to it)."""
|
|
37
|
+
return set(self._reverse.get(file_path, set()))
|
|
38
|
+
|
|
39
|
+
def get_transitive_dependents(self, file_path: str,
|
|
40
|
+
max_depth: int = 10) -> set[str]:
|
|
41
|
+
"""BFS to find all files transitively impacted by changes to file_path."""
|
|
42
|
+
visited: set[str] = set()
|
|
43
|
+
queue = [file_path]
|
|
44
|
+
depth = 0
|
|
45
|
+
while queue and depth < max_depth:
|
|
46
|
+
next_queue = []
|
|
47
|
+
for node in queue:
|
|
48
|
+
for dependent in self._reverse.get(node, set()):
|
|
49
|
+
if dependent not in visited:
|
|
50
|
+
visited.add(dependent)
|
|
51
|
+
next_queue.append(dependent)
|
|
52
|
+
queue = next_queue
|
|
53
|
+
depth += 1
|
|
54
|
+
return visited
|
|
55
|
+
|
|
56
|
+
def compute_impact_set(self, changed_files: list[str]) -> set[str]:
|
|
57
|
+
"""Union of transitive dependents for all changed files."""
|
|
58
|
+
impact: set[str] = set()
|
|
59
|
+
for f in changed_files:
|
|
60
|
+
impact |= self.get_transitive_dependents(f)
|
|
61
|
+
return impact - set(changed_files) # exclude the changed files themselves
|
|
62
|
+
|
|
63
|
+
def all_files(self) -> set[str]:
|
|
64
|
+
return set(self._deps.keys())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _extract_imports(source_code: str, file_path: str = "") -> list[str]:
|
|
68
|
+
"""Extract imported module names from Python source using AST."""
|
|
69
|
+
imports: list[str] = []
|
|
70
|
+
try:
|
|
71
|
+
tree = ast.parse(source_code, filename=file_path)
|
|
72
|
+
except SyntaxError:
|
|
73
|
+
return imports
|
|
74
|
+
for node in ast.walk(tree):
|
|
75
|
+
if isinstance(node, ast.Import):
|
|
76
|
+
for alias in node.names:
|
|
77
|
+
imports.append(alias.name)
|
|
78
|
+
elif isinstance(node, ast.ImportFrom):
|
|
79
|
+
if node.module:
|
|
80
|
+
imports.append(node.module)
|
|
81
|
+
return imports
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _module_to_file(module_name: str, root: Path,
|
|
85
|
+
known_files: set[str]) -> Optional[str]:
|
|
86
|
+
"""Convert a dotted module name to a file path if it exists in the project."""
|
|
87
|
+
rel = module_name.replace(".", "/")
|
|
88
|
+
candidates = [
|
|
89
|
+
str(root / f"{rel}.py"),
|
|
90
|
+
str(root / rel / "__init__.py"),
|
|
91
|
+
]
|
|
92
|
+
for c in candidates:
|
|
93
|
+
if c in known_files:
|
|
94
|
+
return c
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_impact_graph(root: Path, glob_pattern: str = "**/*.py") -> ImpactGraph:
|
|
99
|
+
"""Scan all Python files under root and build the import dependency graph."""
|
|
100
|
+
graph = ImpactGraph()
|
|
101
|
+
py_files = list(root.glob(glob_pattern))
|
|
102
|
+
known_files = {str(f) for f in py_files}
|
|
103
|
+
|
|
104
|
+
for py_file in py_files:
|
|
105
|
+
try:
|
|
106
|
+
source = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
107
|
+
except Exception:
|
|
108
|
+
continue
|
|
109
|
+
raw_imports = _extract_imports(source, str(py_file))
|
|
110
|
+
resolved: list[str] = []
|
|
111
|
+
for mod in raw_imports:
|
|
112
|
+
resolved_path = _module_to_file(mod, root, known_files)
|
|
113
|
+
if resolved_path:
|
|
114
|
+
resolved.append(resolved_path)
|
|
115
|
+
graph.add_file(str(py_file), resolved)
|
|
116
|
+
|
|
117
|
+
return graph
|