misterdev 0.2.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.
- misterdev/__init__.py +3 -0
- misterdev/agent.py +2166 -0
- misterdev/agent_helpers.py +194 -0
- misterdev/analyzers/__init__.py +0 -0
- misterdev/analyzers/project_analyzer/__init__.py +246 -0
- misterdev/analyzers/project_analyzer/detection.py +146 -0
- misterdev/analyzers/project_analyzer/merge.py +137 -0
- misterdev/analyzers/project_analyzer/overview.py +312 -0
- misterdev/analyzers/project_analyzer/prompts.py +83 -0
- misterdev/cli.py +370 -0
- misterdev/config.py +521 -0
- misterdev/core/__init__.py +0 -0
- misterdev/core/audit.py +88 -0
- misterdev/core/config.py +10 -0
- misterdev/core/context/__init__.py +0 -0
- misterdev/core/context/change_tracker.py +218 -0
- misterdev/core/context/contracts/__init__.py +63 -0
- misterdev/core/context/contracts/_log.py +9 -0
- misterdev/core/context/contracts/_text.py +12 -0
- misterdev/core/context/contracts/extraction.py +30 -0
- misterdev/core/context/contracts/python_generic.py +42 -0
- misterdev/core/context/contracts/registry.py +127 -0
- misterdev/core/context/contracts/rust_line.py +225 -0
- misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
- misterdev/core/context/lsp.py +174 -0
- misterdev/core/context/scratchpad.py +111 -0
- misterdev/core/context/topography/__init__.py +43 -0
- misterdev/core/context/topography/_log.py +10 -0
- misterdev/core/context/topography/cache.py +91 -0
- misterdev/core/context/topography/engine.py +172 -0
- misterdev/core/context/topography/graph.py +675 -0
- misterdev/core/context/topography/nodes.py +55 -0
- misterdev/core/context/topography/parsers.py +95 -0
- misterdev/core/context/topography/syntax.py +54 -0
- misterdev/core/economics/__init__.py +0 -0
- misterdev/core/economics/context_budget.py +175 -0
- misterdev/core/economics/embeddings.py +232 -0
- misterdev/core/economics/free_models.py +108 -0
- misterdev/core/economics/llm_cache.py +105 -0
- misterdev/core/economics/model_catalog.py +79 -0
- misterdev/core/economics/model_ledger.py +331 -0
- misterdev/core/economics/model_selector.py +281 -0
- misterdev/core/execution/__init__.py +0 -0
- misterdev/core/execution/bounded.py +50 -0
- misterdev/core/execution/container.py +221 -0
- misterdev/core/execution/error_classifier.py +366 -0
- misterdev/core/execution/error_resolver.py +201 -0
- misterdev/core/execution/governance.py +283 -0
- misterdev/core/execution/outcomes.py +50 -0
- misterdev/core/execution/progress.py +120 -0
- misterdev/core/execution/project.py +231 -0
- misterdev/core/execution/registry.py +97 -0
- misterdev/core/execution/runtime.py +279 -0
- misterdev/core/gitcmd.py +39 -0
- misterdev/core/integration/__init__.py +0 -0
- misterdev/core/integration/mcp.py +368 -0
- misterdev/core/integration/mcp_gather.py +186 -0
- misterdev/core/models.py +35 -0
- misterdev/core/modes.py +184 -0
- misterdev/core/planning/__init__.py +0 -0
- misterdev/core/planning/advisor.py +89 -0
- misterdev/core/planning/assessment.py +135 -0
- misterdev/core/planning/decomposer.py +387 -0
- misterdev/core/planning/metacognition.py +103 -0
- misterdev/core/planning/sovereign.py +308 -0
- misterdev/core/planning/targets.py +201 -0
- misterdev/core/reporting/__init__.py +0 -0
- misterdev/core/reporting/report.py +377 -0
- misterdev/core/reporting/report_view.py +151 -0
- misterdev/core/task.py +163 -0
- misterdev/core/verification/__init__.py +0 -0
- misterdev/core/verification/claim_verifier.py +210 -0
- misterdev/core/verification/critic.py +324 -0
- misterdev/core/verification/gatekeeper/__init__.py +631 -0
- misterdev/core/verification/gatekeeper/constants.py +138 -0
- misterdev/core/verification/gatekeeper/helpers.py +28 -0
- misterdev/core/verification/goal_check.py +219 -0
- misterdev/core/verification/independent.py +68 -0
- misterdev/core/verification/mutation_gate.py +221 -0
- misterdev/core/verification/preflight.py +95 -0
- misterdev/core/verification/spec_tests.py +175 -0
- misterdev/core/verification/validator.py +495 -0
- misterdev/core/verification/vision_verify.py +185 -0
- misterdev/core/verification/web_verify.py +408 -0
- misterdev/environments/__init__.py +0 -0
- misterdev/environments/base_env.py +18 -0
- misterdev/environments/container_env.py +87 -0
- misterdev/environments/venv_env.py +42 -0
- misterdev/llm/__init__.py +0 -0
- misterdev/llm/client/__init__.py +152 -0
- misterdev/llm/client/base.py +382 -0
- misterdev/llm/client/edits.py +70 -0
- misterdev/llm/client/embeddings.py +121 -0
- misterdev/llm/client/errors.py +134 -0
- misterdev/llm/client/providers.py +535 -0
- misterdev/llm/client/response.py +24 -0
- misterdev/llm/prompt_manager.py +82 -0
- misterdev/llm/responses/__init__.py +34 -0
- misterdev/llm/responses/apply.py +131 -0
- misterdev/llm/responses/json_extract.py +80 -0
- misterdev/llm/responses/models.py +43 -0
- misterdev/llm/responses/parsing.py +494 -0
- misterdev/logging_setup.py +20 -0
- misterdev/mcp_server.py +208 -0
- misterdev/nl_cli.py +149 -0
- misterdev/plugins.py +115 -0
- misterdev/py.typed +0 -0
- misterdev/task_executors/__init__.py +0 -0
- misterdev/task_executors/base_executor.py +10 -0
- misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
- misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
- misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
- misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
- misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
- misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
- misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
- misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
- misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
- misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
- misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
- misterdev/tools/__init__.py +19 -0
- misterdev/tools/base_tool.py +14 -0
- misterdev/tools/command.py +75 -0
- misterdev/tools/file_io.py +69 -0
- misterdev/tools/formatter.py +26 -0
- misterdev/tools/git_tool.py +90 -0
- misterdev/utils/__init__.py +0 -0
- misterdev/utils/file_utils.py +169 -0
- misterdev/utils/process.py +23 -0
- misterdev-0.2.0.dist-info/METADATA +326 -0
- misterdev-0.2.0.dist-info/RECORD +136 -0
- misterdev-0.2.0.dist-info/WHEEL +5 -0
- misterdev-0.2.0.dist-info/entry_points.txt +3 -0
- misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
- misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
- misterdev-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Shared package logger.
|
|
2
|
+
|
|
3
|
+
Named for the package (not this submodule) so log output keeps the pre-split
|
|
4
|
+
logger name ``misterdev.core.context.topography`` after the
|
|
5
|
+
god-module was broken into sections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from misterdev.logging_setup import setup_logger
|
|
9
|
+
|
|
10
|
+
logger = setup_logger(__name__.rsplit(".", 1)[0])
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Disk-backed per-file symbol cache, keyed by content hash + lang + format."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional, Set, Any
|
|
7
|
+
|
|
8
|
+
from misterdev.utils.file_utils import atomic_write_json
|
|
9
|
+
|
|
10
|
+
from ._log import logger
|
|
11
|
+
from .nodes import SymbolNode, _symbol_to_dict, _symbol_from_dict
|
|
12
|
+
|
|
13
|
+
# Bump when the parse output shape or any _traverse_* logic changes, so a stale
|
|
14
|
+
# on-disk cache from an older grammar/format is discarded rather than served.
|
|
15
|
+
_CACHE_FORMAT_VERSION = 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _TopographyCache:
|
|
19
|
+
"""Disk-backed per-file symbol cache, keyed by content hash + lang + format.
|
|
20
|
+
|
|
21
|
+
Maps a project-relative path to ``{"key": <hash>, "symbols": [...]}`` where
|
|
22
|
+
the key folds the file's content sha256, its language, and the cache-format
|
|
23
|
+
version. An entry is only reused when the recomputed key matches, so an edit
|
|
24
|
+
(content change), a language change, or a format bump all invalidate it; mtime
|
|
25
|
+
is never consulted. Every read/write is best-effort: a corrupt or unreadable
|
|
26
|
+
cache degrades to an empty in-memory map and a full parse, never a raise and
|
|
27
|
+
never stale symbols.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, path: Path):
|
|
31
|
+
self.path = Path(path)
|
|
32
|
+
self.entries: Dict[str, Dict[str, Any]] = {}
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def make_key(content_bytes: bytes, lang: str) -> str:
|
|
36
|
+
h = hashlib.sha256()
|
|
37
|
+
h.update(f"{_CACHE_FORMAT_VERSION}\x00{lang}\x00".encode("utf-8"))
|
|
38
|
+
h.update(content_bytes)
|
|
39
|
+
return h.hexdigest()
|
|
40
|
+
|
|
41
|
+
def load(self) -> None:
|
|
42
|
+
if not self.path.exists():
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
46
|
+
except (OSError, json.JSONDecodeError, ValueError) as e:
|
|
47
|
+
logger.debug(f"Topography cache unreadable, starting fresh: {e}")
|
|
48
|
+
self.entries = {}
|
|
49
|
+
return
|
|
50
|
+
if (
|
|
51
|
+
not isinstance(data, dict)
|
|
52
|
+
or data.get("format") != _CACHE_FORMAT_VERSION
|
|
53
|
+
or not isinstance(data.get("files"), dict)
|
|
54
|
+
):
|
|
55
|
+
logger.debug("Topography cache format mismatch; starting fresh")
|
|
56
|
+
self.entries = {}
|
|
57
|
+
return
|
|
58
|
+
self.entries = data["files"]
|
|
59
|
+
|
|
60
|
+
def get(self, rel_path: str, key: str) -> Optional[List["SymbolNode"]]:
|
|
61
|
+
"""Cached symbols for ``rel_path`` iff its key matches, else None."""
|
|
62
|
+
entry = self.entries.get(rel_path)
|
|
63
|
+
if not isinstance(entry, dict) or entry.get("key") != key:
|
|
64
|
+
return None
|
|
65
|
+
raw = entry.get("symbols")
|
|
66
|
+
if not isinstance(raw, list):
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
return [_symbol_from_dict(d) for d in raw]
|
|
70
|
+
except (KeyError, TypeError) as e:
|
|
71
|
+
logger.debug(f"Topography cache entry for {rel_path} malformed: {e}")
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def put(self, rel_path: str, key: str, symbols: List["SymbolNode"]) -> None:
|
|
75
|
+
self.entries[rel_path] = {
|
|
76
|
+
"key": key,
|
|
77
|
+
"symbols": [_symbol_to_dict(s) for s in symbols],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def prune(self, live_paths: Set[str]) -> None:
|
|
81
|
+
"""Drop entries for files no longer present in the source tree."""
|
|
82
|
+
for stale in [p for p in self.entries if p not in live_paths]:
|
|
83
|
+
del self.entries[stale]
|
|
84
|
+
|
|
85
|
+
def save(self) -> None:
|
|
86
|
+
try:
|
|
87
|
+
atomic_write_json(
|
|
88
|
+
self.path, {"format": _CACHE_FORMAT_VERSION, "files": self.entries}
|
|
89
|
+
)
|
|
90
|
+
except OSError as e:
|
|
91
|
+
logger.debug(f"Topography cache write failed (non-fatal): {e}")
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""The Topography Engine: a lazy-loaded facade over the symbol graph."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional, Set, Any
|
|
5
|
+
|
|
6
|
+
from ._log import logger
|
|
7
|
+
from .nodes import SymbolNode
|
|
8
|
+
from .graph import SymbolGraph
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TopographyEngine:
|
|
12
|
+
"""Topography Engine with Vector Persistence and Lazy Loading."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, project_path: Path, llm_client: Any, golden_paths=None):
|
|
15
|
+
self.project_path = project_path
|
|
16
|
+
self.llm = llm_client
|
|
17
|
+
self.graph = SymbolGraph(project_path, golden_paths=golden_paths)
|
|
18
|
+
self._initialized = False
|
|
19
|
+
|
|
20
|
+
def initialize(self, force: bool = False):
|
|
21
|
+
if self._initialized and not force:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
logger.info("Initializing Topography Engine...")
|
|
25
|
+
self.graph.build()
|
|
26
|
+
logger.info(f"Symbol graph: {len(self.graph.symbols)} symbols indexed")
|
|
27
|
+
self._initialized = True
|
|
28
|
+
|
|
29
|
+
def invalidate(self) -> None:
|
|
30
|
+
"""Mark the symbol graph stale so the next access rebuilds it.
|
|
31
|
+
|
|
32
|
+
The graph is built once and then never reflected the edits tasks made, so
|
|
33
|
+
later tasks saw a stale map — missing new symbols, listing deleted ones —
|
|
34
|
+
which weakens reference discovery and the dangling-reference gate. Calling
|
|
35
|
+
this after a task changes the tree makes the next task rebuild from the
|
|
36
|
+
current on-disk state. Rebuild is cheap: the per-file symbol cache is
|
|
37
|
+
content-hashed, so only files that actually changed are re-parsed.
|
|
38
|
+
"""
|
|
39
|
+
self._initialized = False
|
|
40
|
+
|
|
41
|
+
def get_file_outline(self, file_path: str) -> str:
|
|
42
|
+
self.initialize()
|
|
43
|
+
return self.graph.file_outline(file_path)
|
|
44
|
+
|
|
45
|
+
def get_file_symbols(self, file_path: str):
|
|
46
|
+
self.initialize()
|
|
47
|
+
return self.graph.file_symbols(file_path)
|
|
48
|
+
|
|
49
|
+
def get_project_outline(self) -> str:
|
|
50
|
+
self.initialize()
|
|
51
|
+
return self.graph.project_outline()
|
|
52
|
+
|
|
53
|
+
def get_context_for_task(
|
|
54
|
+
self,
|
|
55
|
+
query: str,
|
|
56
|
+
related_files: List[str],
|
|
57
|
+
max_symbols: int = 30,
|
|
58
|
+
ranker=None,
|
|
59
|
+
exclude_files: Optional[Set[str]] = None,
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Retrieves functional neighborhood and semantic context. Triggers lazy init.
|
|
62
|
+
|
|
63
|
+
When more candidate symbols are found than fit (``max_symbols``) and a
|
|
64
|
+
semantic ``ranker`` is supplied, the kept symbols are the ones most
|
|
65
|
+
relevant to ``query`` rather than an arbitrary slice.
|
|
66
|
+
|
|
67
|
+
``exclude_files`` names files already sent verbatim IN FULL by another
|
|
68
|
+
context section (small target files in code_context). Their own symbols
|
|
69
|
+
are dropped here so the same code isn't duplicated across two sections;
|
|
70
|
+
their cross-file call-neighbors are still surfaced. Files shown only as a
|
|
71
|
+
windowed excerpt must NOT be excluded — their out-of-window symbols are
|
|
72
|
+
complementary, not duplicate.
|
|
73
|
+
"""
|
|
74
|
+
self.initialize()
|
|
75
|
+
excluded = exclude_files or set()
|
|
76
|
+
|
|
77
|
+
context_symbols: Set[str] = set()
|
|
78
|
+
for file in related_files:
|
|
79
|
+
for key, sym in self.graph.symbols.items():
|
|
80
|
+
if sym.file_path == file:
|
|
81
|
+
if file not in excluded:
|
|
82
|
+
context_symbols.add(key)
|
|
83
|
+
context_symbols.update(sym.outgoing_calls)
|
|
84
|
+
context_symbols.update(sym.incoming_calls)
|
|
85
|
+
|
|
86
|
+
if excluded:
|
|
87
|
+
context_symbols = {
|
|
88
|
+
key
|
|
89
|
+
for key in context_symbols
|
|
90
|
+
if key not in self.graph.symbols
|
|
91
|
+
or self.graph.symbols[key].file_path not in excluded
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if not context_symbols:
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
# Cap to avoid blowing LLM context, keeping the most task-relevant
|
|
98
|
+
# symbols when a semantic ranker is available.
|
|
99
|
+
if ranker is not None and len(context_symbols) > max_symbols:
|
|
100
|
+
candidates = {
|
|
101
|
+
key: self.graph.symbols[key].content
|
|
102
|
+
for key in context_symbols
|
|
103
|
+
if key in self.graph.symbols
|
|
104
|
+
}
|
|
105
|
+
symbol_list = ranker.top_k(query, candidates, max_symbols)
|
|
106
|
+
else:
|
|
107
|
+
symbol_list = list(context_symbols)[:max_symbols]
|
|
108
|
+
|
|
109
|
+
output = "## Topological Context\n"
|
|
110
|
+
by_file: Dict[str, List[SymbolNode]] = {}
|
|
111
|
+
for key in symbol_list:
|
|
112
|
+
if key in self.graph.symbols:
|
|
113
|
+
sym = self.graph.symbols[key]
|
|
114
|
+
by_file.setdefault(sym.file_path, []).append(sym)
|
|
115
|
+
|
|
116
|
+
for file_path, syms in by_file.items():
|
|
117
|
+
output += f"\n### Symbols in {file_path}\n"
|
|
118
|
+
for sym in syms:
|
|
119
|
+
output += f"\n# {sym.kind.upper()}: {sym.name}\n{sym.content}\n"
|
|
120
|
+
|
|
121
|
+
if len(context_symbols) > max_symbols:
|
|
122
|
+
output += (
|
|
123
|
+
f"\n(... {len(context_symbols) - max_symbols} more symbols omitted)\n"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return output
|
|
127
|
+
|
|
128
|
+
def reference_sites(self, target_files: List[str], max_refs: int = 80) -> str:
|
|
129
|
+
"""Every EXTERNAL call site of the symbols defined in ``target_files``.
|
|
130
|
+
|
|
131
|
+
A delete/rename/refactor task must update every reference to the symbol
|
|
132
|
+
it changes, but the referencing files often sit outside the task's
|
|
133
|
+
declared scope — so the model discovers them one build-error at a time
|
|
134
|
+
and runs out of attempts (whack-a-mole). Listing all of them up front,
|
|
135
|
+
with exact file:line, lets one attempt update them completely. Only
|
|
136
|
+
references OUTSIDE the target files are listed (in-file uses are already
|
|
137
|
+
visible in code_context). Returns '' when there are none.
|
|
138
|
+
"""
|
|
139
|
+
self.initialize()
|
|
140
|
+
targets = set(target_files)
|
|
141
|
+
blocks: List[str] = []
|
|
142
|
+
shown = 0
|
|
143
|
+
for sym in self.graph.symbols.values():
|
|
144
|
+
if sym.file_path not in targets or not sym.incoming_calls:
|
|
145
|
+
continue
|
|
146
|
+
sites = set()
|
|
147
|
+
for caller_key in sym.incoming_calls:
|
|
148
|
+
caller = self.graph.symbols.get(caller_key)
|
|
149
|
+
if caller is not None and caller.file_path not in targets:
|
|
150
|
+
sites.add((caller.file_path, caller.start_line, caller.name))
|
|
151
|
+
if not sites:
|
|
152
|
+
continue
|
|
153
|
+
lines = [f"- `{sym.name}` ({sym.kind}) is referenced by:"]
|
|
154
|
+
for fp, ln, nm in sorted(sites):
|
|
155
|
+
if shown >= max_refs:
|
|
156
|
+
lines.append(" - (... more references omitted)")
|
|
157
|
+
break
|
|
158
|
+
lines.append(f" - {fp}:{ln} (in {nm})")
|
|
159
|
+
shown += 1
|
|
160
|
+
blocks.append("\n".join(lines))
|
|
161
|
+
if shown >= max_refs:
|
|
162
|
+
break
|
|
163
|
+
if not blocks:
|
|
164
|
+
return ""
|
|
165
|
+
return (
|
|
166
|
+
"## Complete External Reference Sites\n"
|
|
167
|
+
"Every reference to a symbol defined in the files you are editing "
|
|
168
|
+
"that lives OUTSIDE those files. If this task removes, renames, or "
|
|
169
|
+
"changes the signature of such a symbol, you MUST update ALL of these "
|
|
170
|
+
"sites in this attempt — the build fails on any you miss:\n"
|
|
171
|
+
+ "\n".join(blocks)
|
|
172
|
+
)
|