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.
Files changed (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. 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