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.
Files changed (136) hide show
  1. misterdev/__init__.py +3 -0
  2. misterdev/agent.py +2166 -0
  3. misterdev/agent_helpers.py +194 -0
  4. misterdev/analyzers/__init__.py +0 -0
  5. misterdev/analyzers/project_analyzer/__init__.py +246 -0
  6. misterdev/analyzers/project_analyzer/detection.py +146 -0
  7. misterdev/analyzers/project_analyzer/merge.py +137 -0
  8. misterdev/analyzers/project_analyzer/overview.py +312 -0
  9. misterdev/analyzers/project_analyzer/prompts.py +83 -0
  10. misterdev/cli.py +370 -0
  11. misterdev/config.py +521 -0
  12. misterdev/core/__init__.py +0 -0
  13. misterdev/core/audit.py +88 -0
  14. misterdev/core/config.py +10 -0
  15. misterdev/core/context/__init__.py +0 -0
  16. misterdev/core/context/change_tracker.py +218 -0
  17. misterdev/core/context/contracts/__init__.py +63 -0
  18. misterdev/core/context/contracts/_log.py +9 -0
  19. misterdev/core/context/contracts/_text.py +12 -0
  20. misterdev/core/context/contracts/extraction.py +30 -0
  21. misterdev/core/context/contracts/python_generic.py +42 -0
  22. misterdev/core/context/contracts/registry.py +127 -0
  23. misterdev/core/context/contracts/rust_line.py +225 -0
  24. misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
  25. misterdev/core/context/lsp.py +174 -0
  26. misterdev/core/context/scratchpad.py +111 -0
  27. misterdev/core/context/topography/__init__.py +43 -0
  28. misterdev/core/context/topography/_log.py +10 -0
  29. misterdev/core/context/topography/cache.py +91 -0
  30. misterdev/core/context/topography/engine.py +172 -0
  31. misterdev/core/context/topography/graph.py +675 -0
  32. misterdev/core/context/topography/nodes.py +55 -0
  33. misterdev/core/context/topography/parsers.py +95 -0
  34. misterdev/core/context/topography/syntax.py +54 -0
  35. misterdev/core/economics/__init__.py +0 -0
  36. misterdev/core/economics/context_budget.py +175 -0
  37. misterdev/core/economics/embeddings.py +232 -0
  38. misterdev/core/economics/free_models.py +108 -0
  39. misterdev/core/economics/llm_cache.py +105 -0
  40. misterdev/core/economics/model_catalog.py +79 -0
  41. misterdev/core/economics/model_ledger.py +331 -0
  42. misterdev/core/economics/model_selector.py +281 -0
  43. misterdev/core/execution/__init__.py +0 -0
  44. misterdev/core/execution/bounded.py +50 -0
  45. misterdev/core/execution/container.py +221 -0
  46. misterdev/core/execution/error_classifier.py +366 -0
  47. misterdev/core/execution/error_resolver.py +201 -0
  48. misterdev/core/execution/governance.py +283 -0
  49. misterdev/core/execution/outcomes.py +50 -0
  50. misterdev/core/execution/progress.py +120 -0
  51. misterdev/core/execution/project.py +231 -0
  52. misterdev/core/execution/registry.py +97 -0
  53. misterdev/core/execution/runtime.py +279 -0
  54. misterdev/core/gitcmd.py +39 -0
  55. misterdev/core/integration/__init__.py +0 -0
  56. misterdev/core/integration/mcp.py +368 -0
  57. misterdev/core/integration/mcp_gather.py +186 -0
  58. misterdev/core/models.py +35 -0
  59. misterdev/core/modes.py +184 -0
  60. misterdev/core/planning/__init__.py +0 -0
  61. misterdev/core/planning/advisor.py +89 -0
  62. misterdev/core/planning/assessment.py +135 -0
  63. misterdev/core/planning/decomposer.py +387 -0
  64. misterdev/core/planning/metacognition.py +103 -0
  65. misterdev/core/planning/sovereign.py +308 -0
  66. misterdev/core/planning/targets.py +201 -0
  67. misterdev/core/reporting/__init__.py +0 -0
  68. misterdev/core/reporting/report.py +377 -0
  69. misterdev/core/reporting/report_view.py +151 -0
  70. misterdev/core/task.py +163 -0
  71. misterdev/core/verification/__init__.py +0 -0
  72. misterdev/core/verification/claim_verifier.py +210 -0
  73. misterdev/core/verification/critic.py +324 -0
  74. misterdev/core/verification/gatekeeper/__init__.py +631 -0
  75. misterdev/core/verification/gatekeeper/constants.py +138 -0
  76. misterdev/core/verification/gatekeeper/helpers.py +28 -0
  77. misterdev/core/verification/goal_check.py +219 -0
  78. misterdev/core/verification/independent.py +68 -0
  79. misterdev/core/verification/mutation_gate.py +221 -0
  80. misterdev/core/verification/preflight.py +95 -0
  81. misterdev/core/verification/spec_tests.py +175 -0
  82. misterdev/core/verification/validator.py +495 -0
  83. misterdev/core/verification/vision_verify.py +185 -0
  84. misterdev/core/verification/web_verify.py +408 -0
  85. misterdev/environments/__init__.py +0 -0
  86. misterdev/environments/base_env.py +18 -0
  87. misterdev/environments/container_env.py +87 -0
  88. misterdev/environments/venv_env.py +42 -0
  89. misterdev/llm/__init__.py +0 -0
  90. misterdev/llm/client/__init__.py +152 -0
  91. misterdev/llm/client/base.py +382 -0
  92. misterdev/llm/client/edits.py +70 -0
  93. misterdev/llm/client/embeddings.py +121 -0
  94. misterdev/llm/client/errors.py +134 -0
  95. misterdev/llm/client/providers.py +535 -0
  96. misterdev/llm/client/response.py +24 -0
  97. misterdev/llm/prompt_manager.py +82 -0
  98. misterdev/llm/responses/__init__.py +34 -0
  99. misterdev/llm/responses/apply.py +131 -0
  100. misterdev/llm/responses/json_extract.py +80 -0
  101. misterdev/llm/responses/models.py +43 -0
  102. misterdev/llm/responses/parsing.py +494 -0
  103. misterdev/logging_setup.py +20 -0
  104. misterdev/mcp_server.py +208 -0
  105. misterdev/nl_cli.py +149 -0
  106. misterdev/plugins.py +115 -0
  107. misterdev/py.typed +0 -0
  108. misterdev/task_executors/__init__.py +0 -0
  109. misterdev/task_executors/base_executor.py +10 -0
  110. misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
  111. misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
  112. misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
  113. misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
  114. misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
  115. misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
  116. misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
  117. misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
  118. misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
  119. misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
  120. misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
  121. misterdev/tools/__init__.py +19 -0
  122. misterdev/tools/base_tool.py +14 -0
  123. misterdev/tools/command.py +75 -0
  124. misterdev/tools/file_io.py +69 -0
  125. misterdev/tools/formatter.py +26 -0
  126. misterdev/tools/git_tool.py +90 -0
  127. misterdev/utils/__init__.py +0 -0
  128. misterdev/utils/file_utils.py +169 -0
  129. misterdev/utils/process.py +23 -0
  130. misterdev-0.2.0.dist-info/METADATA +326 -0
  131. misterdev-0.2.0.dist-info/RECORD +136 -0
  132. misterdev-0.2.0.dist-info/WHEEL +5 -0
  133. misterdev-0.2.0.dist-info/entry_points.txt +3 -0
  134. misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
  135. misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
  136. 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