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,131 @@
|
|
|
1
|
+
"""Apply anchored SEARCH/REPLACE hunks against on-disk file content."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from .models import SearchReplaceEdit, EditConflictError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def apply_search_replace(original: str, edits: List[SearchReplaceEdit]) -> str:
|
|
9
|
+
"""Apply anchored hunks to ``original`` and return the full new content.
|
|
10
|
+
|
|
11
|
+
Each hunk's SEARCH snippet must resolve to exactly one location; a missing
|
|
12
|
+
or ambiguous anchor raises :class:`EditConflictError` so the caller can
|
|
13
|
+
retry rather than write a corrupt file. An empty SEARCH creates a new file
|
|
14
|
+
and is rejected if the file already has content. Matching is line-anchored
|
|
15
|
+
(never a bare substring, which would splice mid-line), with a per-line
|
|
16
|
+
whitespace/line-ending-tolerant comparison that absorbs the trailing-space
|
|
17
|
+
and CRLF drift models commonly introduce without ever matching more than one
|
|
18
|
+
place.
|
|
19
|
+
"""
|
|
20
|
+
content = original
|
|
21
|
+
for edit in edits:
|
|
22
|
+
if not edit.search.strip():
|
|
23
|
+
if content.strip():
|
|
24
|
+
raise EditConflictError(
|
|
25
|
+
f"{edit.path}: empty SEARCH block is only valid for a new "
|
|
26
|
+
"file, but this file already has content. Anchor the edit "
|
|
27
|
+
"to the exact lines you are replacing."
|
|
28
|
+
)
|
|
29
|
+
content = edit.replace
|
|
30
|
+
continue
|
|
31
|
+
content = _apply_one_hunk(content, edit)
|
|
32
|
+
return content
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _apply_one_hunk(content: str, edit: SearchReplaceEdit) -> str:
|
|
36
|
+
spliced, hits = _line_splice(content, edit.search, edit.replace)
|
|
37
|
+
if hits == 1:
|
|
38
|
+
return spliced
|
|
39
|
+
if hits == 0:
|
|
40
|
+
raise EditConflictError(
|
|
41
|
+
f"{edit.path}: SEARCH block not found. It must match the current "
|
|
42
|
+
"file (whitespace aside). Re-read the file and copy the lines "
|
|
43
|
+
"verbatim."
|
|
44
|
+
)
|
|
45
|
+
raise EditConflictError(
|
|
46
|
+
f"{edit.path}: SEARCH block matches {hits} locations. Include more "
|
|
47
|
+
"surrounding context so it identifies exactly one place."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _line_splice(content: str, search: str, replace: str) -> tuple:
|
|
52
|
+
"""Locate ``search`` as a window of whole LINES in ``content`` and replace it.
|
|
53
|
+
|
|
54
|
+
Line-anchored, never a bare substring: a single-line hunk like ``x = 1`` is
|
|
55
|
+
matched against whole lines, so it can never splice into the middle of a
|
|
56
|
+
longer line such as ``x = 10``. Two passes, each requiring exactly one
|
|
57
|
+
matching window (any other count is a conflict and the content is returned
|
|
58
|
+
unmutated):
|
|
59
|
+
|
|
60
|
+
1. right-stripped / CR-free lines — absorbs trailing-space and CRLF drift,
|
|
61
|
+
keeping indentation significant;
|
|
62
|
+
2. fully-stripped lines — absorbs wrong indentation too, then re-indents the
|
|
63
|
+
replacement to the file's actual indentation so formatting is preserved.
|
|
64
|
+
|
|
65
|
+
Replacement lines inherit the file's line ending, so a CRLF file stays CRLF.
|
|
66
|
+
"""
|
|
67
|
+
crlf = "\r\n" in content
|
|
68
|
+
o_lines = content.split("\n")
|
|
69
|
+
s_lines = search.split("\n")
|
|
70
|
+
r_lines = replace.split("\n")
|
|
71
|
+
|
|
72
|
+
rstripped = [ln.replace("\r", "").rstrip() for ln in o_lines]
|
|
73
|
+
ns_r = [ln.replace("\r", "").rstrip() for ln in s_lines]
|
|
74
|
+
k = len(ns_r)
|
|
75
|
+
if k == 0 or k > len(o_lines):
|
|
76
|
+
return content, 0
|
|
77
|
+
hits = [i for i in range(len(rstripped) - k + 1) if rstripped[i : i + k] == ns_r]
|
|
78
|
+
if len(hits) == 1:
|
|
79
|
+
return _join_splice(o_lines, hits[0], k, r_lines, crlf), 1
|
|
80
|
+
if len(hits) > 1:
|
|
81
|
+
return content, len(hits)
|
|
82
|
+
|
|
83
|
+
# Pass 2: ignore indentation, then reapply the file's indentation.
|
|
84
|
+
stripped = [ln.strip() for ln in o_lines]
|
|
85
|
+
ns_s = [ln.strip() for ln in s_lines]
|
|
86
|
+
hits = [i for i in range(len(stripped) - k + 1) if stripped[i : i + k] == ns_s]
|
|
87
|
+
if len(hits) != 1:
|
|
88
|
+
return content, len(hits)
|
|
89
|
+
i = hits[0]
|
|
90
|
+
reindented = _reindent(o_lines[i : i + k], s_lines, r_lines)
|
|
91
|
+
return _join_splice(o_lines, i, k, reindented, crlf), 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _join_splice(
|
|
95
|
+
o_lines: List[str], i: int, k: int, r_lines: List[str], crlf: bool
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Splice ``r_lines`` over ``o_lines[i:i+k]``, giving the replacement lines the
|
|
98
|
+
file's line ending so a CRLF file does not end up with mixed CRLF/LF lines."""
|
|
99
|
+
if crlf:
|
|
100
|
+
r_lines = [ln if ln.endswith("\r") else ln + "\r" for ln in r_lines]
|
|
101
|
+
return "\n".join(o_lines[:i] + r_lines + o_lines[i + k :])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _leading_ws(line: str) -> str:
|
|
105
|
+
return line[: len(line) - len(line.lstrip())]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _reindent(matched: List[str], search: List[str], replace: List[str]) -> List[str]:
|
|
109
|
+
"""Shift ``replace`` by the indent delta between matched and search anchors.
|
|
110
|
+
|
|
111
|
+
The delta is measured on the first non-blank line of each; applying it to
|
|
112
|
+
every non-blank replacement line lands the new code at the file's real
|
|
113
|
+
indentation even when the model used a different indent in its SEARCH.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def first_indent(lines: List[str]) -> str:
|
|
117
|
+
for ln in lines:
|
|
118
|
+
if ln.strip():
|
|
119
|
+
return _leading_ws(ln.replace("\r", ""))
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
target = first_indent(matched)
|
|
123
|
+
source = first_indent(search)
|
|
124
|
+
out = []
|
|
125
|
+
for ln in replace:
|
|
126
|
+
if not ln.strip():
|
|
127
|
+
out.append(ln)
|
|
128
|
+
continue
|
|
129
|
+
body = ln[len(source) :] if ln.startswith(source) else ln.lstrip()
|
|
130
|
+
out.append(target + body)
|
|
131
|
+
return out
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Extract JSON values embedded in prose or code fences from LLM output."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_json_array(response: str, default: Optional[Any] = None) -> Any:
|
|
8
|
+
"""Extract the outermost JSON array from an LLM response.
|
|
9
|
+
|
|
10
|
+
LLMs often wrap a JSON array in prose or code fences; this slices from the
|
|
11
|
+
first ``[`` to the last ``]`` and parses. Returns ``default`` (or ``[]``)
|
|
12
|
+
on any failure. Consolidates the find/slice/loads pattern that was copy-
|
|
13
|
+
pasted across the decomposer, advisor, sovereign, and metacognition.
|
|
14
|
+
"""
|
|
15
|
+
if default is None:
|
|
16
|
+
default = []
|
|
17
|
+
text = (response or "").strip()
|
|
18
|
+
start = text.find("[")
|
|
19
|
+
end = text.rfind("]")
|
|
20
|
+
if start < 0 or end <= start:
|
|
21
|
+
return default
|
|
22
|
+
try:
|
|
23
|
+
return json.loads(text[start : end + 1])
|
|
24
|
+
except (json.JSONDecodeError, ValueError):
|
|
25
|
+
return default
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_balanced_span(text: str, start: int) -> Optional[str]:
|
|
29
|
+
"""Return the balanced ``{...}`` substring beginning at ``start`` (a ``{``).
|
|
30
|
+
|
|
31
|
+
Brace-counts while honoring braces inside double-quoted strings (with
|
|
32
|
+
escapes), so an object containing ``{`` or ``}`` in a string value is
|
|
33
|
+
extracted whole; tolerates the object spanning multiple lines. Returns None
|
|
34
|
+
if the span never balances. Consolidates the string-aware brace scan that was
|
|
35
|
+
duplicated in the goal check and the MCP gather loop.
|
|
36
|
+
"""
|
|
37
|
+
depth = 0
|
|
38
|
+
in_string = False
|
|
39
|
+
escaped = False
|
|
40
|
+
for i in range(start, len(text)):
|
|
41
|
+
ch = text[i]
|
|
42
|
+
if in_string:
|
|
43
|
+
if escaped:
|
|
44
|
+
escaped = False
|
|
45
|
+
elif ch == "\\":
|
|
46
|
+
escaped = True
|
|
47
|
+
elif ch == '"':
|
|
48
|
+
in_string = False
|
|
49
|
+
continue
|
|
50
|
+
if ch == '"':
|
|
51
|
+
in_string = True
|
|
52
|
+
elif ch == "{":
|
|
53
|
+
depth += 1
|
|
54
|
+
elif ch == "}":
|
|
55
|
+
depth -= 1
|
|
56
|
+
if depth == 0:
|
|
57
|
+
return text[start : i + 1]
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_json_object(text: str) -> Optional[dict]:
|
|
62
|
+
"""Return the first parseable top-level JSON object in ``text``, or None.
|
|
63
|
+
|
|
64
|
+
Scans each ``{`` for a balanced, string-aware span and json-loads it; a span
|
|
65
|
+
that fails to parse is skipped and the next ``{`` is tried. Survives leading
|
|
66
|
+
prose or a ```json fence around the object.
|
|
67
|
+
"""
|
|
68
|
+
if not text:
|
|
69
|
+
return None
|
|
70
|
+
start = text.find("{")
|
|
71
|
+
while start != -1:
|
|
72
|
+
span = extract_balanced_span(text, start)
|
|
73
|
+
if span is not None:
|
|
74
|
+
try:
|
|
75
|
+
parsed = json.loads(span)
|
|
76
|
+
return parsed if isinstance(parsed, dict) else None
|
|
77
|
+
except ValueError:
|
|
78
|
+
pass # malformed span; try the next '{'
|
|
79
|
+
start = text.find("{", start + 1)
|
|
80
|
+
return None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Edit shapes and errors shared across the response parsers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class FileEdit:
|
|
8
|
+
path: str
|
|
9
|
+
content: str
|
|
10
|
+
is_new: bool = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SearchReplaceEdit:
|
|
15
|
+
"""A single anchored, surgical edit to one file.
|
|
16
|
+
|
|
17
|
+
``search`` is the exact existing snippet to locate; ``replace`` is what
|
|
18
|
+
takes its place. An empty ``search`` means "create this file" and is only
|
|
19
|
+
valid when the file does not yet exist. Multiple edits may target the same
|
|
20
|
+
path and are applied in order against the evolving content.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
path: str
|
|
24
|
+
search: str
|
|
25
|
+
replace: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EditConflictError(Exception):
|
|
29
|
+
"""A search/replace hunk could not be applied unambiguously.
|
|
30
|
+
|
|
31
|
+
Raised when the SEARCH snippet is absent, matches more than once, or would
|
|
32
|
+
blank/overwrite an existing file. The caller surfaces the message back to
|
|
33
|
+
the model as an error so it retries with a corrected anchor — a partial or
|
|
34
|
+
truncated file is never written.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class _CodeBlock:
|
|
40
|
+
lang: str
|
|
41
|
+
path_hint: str
|
|
42
|
+
content_lines: list
|
|
43
|
+
preceding_text: str
|