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,225 @@
1
+ """Line-based heuristic public-symbol extraction for Rust source.
2
+
3
+ Fallback used when the tree-sitter Rust grammar is unavailable.
4
+ """
5
+
6
+ from typing import Dict, List
7
+
8
+ from ._text import _extract_name
9
+
10
+
11
+ def _extract_rust_symbols(lines: List[str]) -> List[Dict[str, str]]:
12
+ """Extract public API from Rust source.
13
+
14
+ Handles: pub fn, pub(crate) fn, pub struct (with fields), pub enum,
15
+ pub trait (with method signatures), pub type, pub const, impl blocks.
16
+ Collects multi-line signatures up to the opening brace.
17
+ """
18
+ symbols = []
19
+ i = 0
20
+ brace_depth = 0
21
+ while i < len(lines):
22
+ stripped = lines[i].strip()
23
+ line_opens = stripped.count("{") - stripped.count("}")
24
+
25
+ # Skip lines inside bodies (impl, struct, enum, trait blocks)
26
+ if brace_depth > 0:
27
+ brace_depth += line_opens
28
+ i += 1
29
+ continue
30
+
31
+ # Match pub, pub(crate), pub(super)
32
+ if not (stripped.startswith("pub ") or stripped.startswith("pub(")):
33
+ # Check for impl blocks (extract public methods inside)
34
+ if stripped.startswith("impl ") or stripped.startswith("impl<"):
35
+ impl_name = _extract_impl_name(stripped)
36
+ impl_methods = _extract_impl_methods(lines, i)
37
+ for m in impl_methods:
38
+ m["name"] = f"{impl_name}::{m['name']}"
39
+ symbols.extend(impl_methods)
40
+ # Skip past the impl block
41
+ brace_depth += line_opens
42
+ else:
43
+ brace_depth += line_opens
44
+ i += 1
45
+ continue
46
+
47
+ # Strip visibility qualifier to get the declaration
48
+ after_vis = _strip_visibility(stripped)
49
+
50
+ if after_vis.startswith("fn "):
51
+ sig = _collect_signature(lines, i, "{")
52
+ symbols.append(
53
+ {
54
+ "kind": "pub fn",
55
+ "name": _extract_name(after_vis[3:]),
56
+ "signature": sig,
57
+ }
58
+ )
59
+ elif after_vis.startswith("struct "):
60
+ name = _extract_name(after_vis[7:])
61
+ sig = f"pub struct {name}"
62
+ # Collect generic bounds
63
+ generic = _extract_generics(after_vis[7:])
64
+ if generic:
65
+ sig = f"pub struct {name}{generic}"
66
+ fields = _collect_struct_fields(lines, i)
67
+ if fields:
68
+ sig += " { " + ", ".join(fields) + " }"
69
+ symbols.append({"kind": "pub struct", "name": name, "signature": sig})
70
+ elif after_vis.startswith("enum "):
71
+ name = _extract_name(after_vis[5:])
72
+ variants = _collect_enum_variants(lines, i)
73
+ sig = f"pub enum {name}"
74
+ if variants:
75
+ sig += " { " + ", ".join(variants) + " }"
76
+ symbols.append({"kind": "pub enum", "name": name, "signature": sig})
77
+ elif after_vis.startswith("trait "):
78
+ name = _extract_name(after_vis[6:])
79
+ trait_methods = _collect_trait_methods(lines, i)
80
+ sig = f"pub trait {name}"
81
+ if trait_methods:
82
+ sig += " { " + "; ".join(trait_methods) + "; }"
83
+ symbols.append({"kind": "pub trait", "name": name, "signature": sig})
84
+ elif after_vis.startswith("type "):
85
+ symbols.append(
86
+ {
87
+ "kind": "pub type",
88
+ "name": _extract_name(after_vis[5:]),
89
+ "signature": stripped,
90
+ }
91
+ )
92
+ elif after_vis.startswith("const "):
93
+ symbols.append(
94
+ {
95
+ "kind": "pub const",
96
+ "name": _extract_name(after_vis[6:]),
97
+ "signature": stripped,
98
+ }
99
+ )
100
+
101
+ brace_depth += line_opens
102
+ i += 1
103
+
104
+ return symbols
105
+
106
+
107
+ def _strip_visibility(line: str) -> str:
108
+ """Strip pub/pub(crate)/pub(super) prefix."""
109
+ if line.startswith("pub("):
110
+ close = line.find(")")
111
+ if close >= 0:
112
+ return line[close + 1 :].strip()
113
+ if line.startswith("pub "):
114
+ return line[4:].strip()
115
+ return line
116
+
117
+
118
+ def _extract_generics(text: str) -> str:
119
+ """Extract <...> generic parameters."""
120
+ if "<" not in text:
121
+ return ""
122
+ depth = 0
123
+ start = text.find("<")
124
+ for i in range(start, len(text)):
125
+ if text[i] == "<":
126
+ depth += 1
127
+ elif text[i] == ">":
128
+ depth -= 1
129
+ if depth == 0:
130
+ return text[start : i + 1]
131
+ return ""
132
+
133
+
134
+ def _extract_impl_name(line: str) -> str:
135
+ """Extract type name from 'impl<T> Foo<T> for Bar'."""
136
+ # Remove "impl" and optional generics
137
+ rest = line[4:].strip()
138
+ if rest.startswith("<"):
139
+ close = rest.find(">")
140
+ if close >= 0:
141
+ rest = rest[close + 1 :].strip()
142
+ return _extract_name(rest)
143
+
144
+
145
+ def _extract_impl_methods(lines: List[str], start: int) -> List[Dict[str, str]]:
146
+ """Extract pub fn declarations from inside an impl block."""
147
+ methods = []
148
+ depth = 0
149
+ for i in range(start, min(start + 200, len(lines))):
150
+ depth += lines[i].count("{") - lines[i].count("}")
151
+ stripped = lines[i].strip()
152
+ if stripped.startswith("pub fn ") or stripped.startswith("pub(crate) fn "):
153
+ after_vis = _strip_visibility(stripped)
154
+ sig = _collect_signature(lines, i, "{")
155
+ methods.append(
156
+ {
157
+ "kind": "pub fn",
158
+ "name": _extract_name(after_vis[3:]),
159
+ "signature": sig,
160
+ }
161
+ )
162
+ if depth <= 0 and i > start:
163
+ break
164
+ return methods
165
+
166
+
167
+ def _collect_enum_variants(lines: List[str], start: int) -> List[str]:
168
+ """Collect variant names from a Rust enum."""
169
+ variants = []
170
+ depth = 0
171
+ for i in range(start, min(start + 50, len(lines))):
172
+ depth += lines[i].count("{") - lines[i].count("}")
173
+ stripped = lines[i].strip()
174
+ if i > start and depth >= 1 and stripped and not stripped.startswith("//"):
175
+ name = stripped.split("(")[0].split("{")[0].rstrip(",").strip()
176
+ if name and name[0].isupper():
177
+ variants.append(name)
178
+ if depth <= 0 and i > start:
179
+ break
180
+ return variants[:15]
181
+
182
+
183
+ def _collect_trait_methods(lines: List[str], start: int) -> List[str]:
184
+ """Collect fn signatures from a trait definition."""
185
+ methods = []
186
+ depth = 0
187
+ for i in range(start, min(start + 50, len(lines))):
188
+ depth += lines[i].count("{") - lines[i].count("}")
189
+ stripped = lines[i].strip()
190
+ if stripped.startswith("fn "):
191
+ sig = stripped.rstrip(";").rstrip("{").strip()
192
+ methods.append(sig)
193
+ if depth <= 0 and i > start:
194
+ break
195
+ return methods[:10]
196
+
197
+
198
+ def _collect_signature(lines: List[str], start: int, terminator: str) -> str:
199
+ """Collect a multi-line signature up to the terminator character."""
200
+ sig_parts = []
201
+ for i in range(start, min(start + 5, len(lines))):
202
+ sig_parts.append(lines[i].strip())
203
+ if terminator in lines[i]:
204
+ break
205
+ sig = " ".join(sig_parts)
206
+ # Trim at the terminator
207
+ idx = sig.find(terminator)
208
+ if idx >= 0:
209
+ sig = sig[:idx].strip()
210
+ return sig
211
+
212
+
213
+ def _collect_struct_fields(lines: List[str], start: int) -> List[str]:
214
+ """Collect pub field names from a Rust struct."""
215
+ fields = []
216
+ brace_depth = 0
217
+ for i in range(start, min(start + 30, len(lines))):
218
+ line = lines[i].strip()
219
+ brace_depth += line.count("{") - line.count("}")
220
+ if "pub " in line and ":" in line:
221
+ field = line.strip().rstrip(",")
222
+ fields.append(field)
223
+ if brace_depth <= 0 and i > start:
224
+ break
225
+ return fields[:10]
@@ -0,0 +1,141 @@
1
+ """Tree-sitter based public-symbol extraction for Rust source."""
2
+
3
+ from typing import Dict, List, Optional
4
+
5
+ from ._log import logger
6
+
7
+
8
+ def _extract_rust_symbols_ts(content: str) -> List[Dict[str, str]]:
9
+ """Extract public Rust API using tree-sitter, reusing topography's parser.
10
+
11
+ Returns [] when tree-sitter or the Rust grammar is unavailable so the
12
+ caller can fall back to the line-based parser.
13
+ """
14
+ try:
15
+ from misterdev.core.context.topography import _get_ts_parsers
16
+
17
+ parser = _get_ts_parsers().get("rust")
18
+ except Exception as e:
19
+ logger.debug(f"tree-sitter rust parser unavailable: {e}")
20
+ return []
21
+ if parser is None:
22
+ return []
23
+ try:
24
+ tree = parser.parse(bytes(content, "utf8"))
25
+ except Exception as e:
26
+ logger.debug(f"tree-sitter parse failed; using line parser: {e}")
27
+ return []
28
+ symbols: List[Dict[str, str]] = []
29
+ _walk_rust_ts(tree.root_node, content, symbols, parent=None)
30
+ return symbols
31
+
32
+
33
+ def _ts_is_pub(node) -> bool:
34
+ return any(c.type == "visibility_modifier" for c in node.children)
35
+
36
+
37
+ def _ts_field_text(node, field: str, content: str) -> str:
38
+ n = node.child_by_field_name(field)
39
+ return content[n.start_byte : n.end_byte] if n else ""
40
+
41
+
42
+ def _ts_decl(node, content: str) -> str:
43
+ """Declaration text up to the body (captures generics/where, drops body)."""
44
+ body = node.child_by_field_name("body")
45
+ end = body.start_byte if body else node.end_byte
46
+ return " ".join(content[node.start_byte : end].split()).strip()
47
+
48
+
49
+ def _walk_rust_ts(
50
+ node, content: str, symbols: List[Dict[str, str]], parent: Optional[str]
51
+ ):
52
+ t = node.type
53
+ if t == "function_item":
54
+ if _ts_is_pub(node):
55
+ name = _ts_field_text(node, "name", content)
56
+ full = f"{parent}::{name}" if parent else name
57
+ symbols.append(
58
+ {"kind": "pub fn", "name": full, "signature": _ts_decl(node, content)}
59
+ )
60
+ return
61
+ if t == "struct_item" and _ts_is_pub(node):
62
+ name = _ts_field_text(node, "name", content)
63
+ sig = _ts_decl(node, content)
64
+ fields = _ts_pub_members(node, "field_declaration", content)
65
+ if fields:
66
+ sig += " { " + ", ".join(fields) + " }"
67
+ symbols.append({"kind": "pub struct", "name": name, "signature": sig})
68
+ return
69
+ if t == "enum_item" and _ts_is_pub(node):
70
+ name = _ts_field_text(node, "name", content)
71
+ variants = _ts_variant_names(node, content)
72
+ sig = _ts_decl(node, content)
73
+ if variants:
74
+ sig += " { " + ", ".join(variants) + " }"
75
+ symbols.append({"kind": "pub enum", "name": name, "signature": sig})
76
+ return
77
+ if t == "trait_item" and _ts_is_pub(node):
78
+ name = _ts_field_text(node, "name", content)
79
+ methods = _ts_trait_methods(node, content)
80
+ sig = _ts_decl(node, content)
81
+ if methods:
82
+ sig += " { " + "; ".join(methods) + "; }"
83
+ symbols.append({"kind": "pub trait", "name": name, "signature": sig})
84
+ return
85
+ if t == "type_item" and _ts_is_pub(node):
86
+ name = _ts_field_text(node, "name", content)
87
+ text = " ".join(content[node.start_byte : node.end_byte].split()).rstrip(";")
88
+ symbols.append({"kind": "pub type", "name": name, "signature": text})
89
+ return
90
+ if t == "impl_item":
91
+ type_node = node.child_by_field_name("type")
92
+ body = node.child_by_field_name("body")
93
+ if type_node is not None and body is not None:
94
+ impl_name = (
95
+ content[type_node.start_byte : type_node.end_byte].split("<")[0].strip()
96
+ )
97
+ for c in body.children:
98
+ _walk_rust_ts(c, content, symbols, parent=impl_name)
99
+ return
100
+ for c in node.children:
101
+ _walk_rust_ts(c, content, symbols, parent)
102
+
103
+
104
+ def _ts_pub_members(node, child_type: str, content: str) -> List[str]:
105
+ body = node.child_by_field_name("body")
106
+ members = []
107
+ if body is not None:
108
+ for c in body.children:
109
+ if c.type == child_type and any(
110
+ g.type == "visibility_modifier" for g in c.children
111
+ ):
112
+ members.append(
113
+ " ".join(content[c.start_byte : c.end_byte].split()).rstrip(",")
114
+ )
115
+ return members[:10]
116
+
117
+
118
+ def _ts_variant_names(node, content: str) -> List[str]:
119
+ body = node.child_by_field_name("body")
120
+ names = []
121
+ if body is not None:
122
+ for c in body.children:
123
+ if c.type == "enum_variant":
124
+ n = c.child_by_field_name("name")
125
+ if n is not None:
126
+ names.append(content[n.start_byte : n.end_byte])
127
+ return names[:15]
128
+
129
+
130
+ def _ts_trait_methods(node, content: str) -> List[str]:
131
+ body = node.child_by_field_name("body")
132
+ methods = []
133
+ if body is not None:
134
+ for c in body.children:
135
+ if c.type in ("function_signature_item", "function_item"):
136
+ b = c.child_by_field_name("body")
137
+ end = b.start_byte if b is not None else c.end_byte
138
+ methods.append(
139
+ " ".join(content[c.start_byte : end].split()).rstrip(";")
140
+ )
141
+ return methods[:10]
@@ -0,0 +1,174 @@
1
+ """Optional LSP-based semantic diagnostics.
2
+
3
+ A language server surfaces errors the tree-sitter syntax gate cannot see —
4
+ undefined names, type mismatches, unresolved imports — across languages without
5
+ per-project linter configuration. This is strictly best-effort: the work runs in
6
+ a daemon worker thread with a hard timeout and returns ``None`` on any problem
7
+ (multilspy missing, unsupported language, server slow/unavailable), so it can
8
+ never block or slow a build. It is off unless ``orchestrator.lsp_diagnostics``
9
+ is enabled, and even then a timeout silently skips the check rather than failing.
10
+ """
11
+
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional
14
+
15
+ from misterdev.core.execution.bounded import run_bounded
16
+ from misterdev.logging_setup import setup_logger
17
+
18
+ logger = setup_logger(__name__)
19
+
20
+ # Our internal language ids -> multilspy code_language values. Languages without
21
+ # a multilspy server (c, cpp, swift) are intentionally absent -> gate skipped.
22
+ _LANG_MAP: Dict[str, str] = {
23
+ "python": "python",
24
+ "rust": "rust",
25
+ "typescript": "typescript",
26
+ "javascript": "javascript",
27
+ "csharp": "csharp",
28
+ "kotlin": "kotlin",
29
+ "java": "java",
30
+ "go": "go",
31
+ "ruby": "ruby",
32
+ }
33
+
34
+ _LSP_SEVERITY_ERROR = 1 # LSP DiagnosticSeverity.Error
35
+
36
+ # Per-file diagnostic settle wait bounds (seconds). The actual wait scales down
37
+ # with file count so the total stays under the caller's hard timeout.
38
+ _MIN_FILE_WAIT = 0.3
39
+ _MAX_FILE_WAIT = 1.5
40
+
41
+
42
+ def _per_file_wait(num_files: int, settle_budget: float) -> float:
43
+ """Settle wait per file: the budget split across files, clamped to bounds.
44
+
45
+ Keeps the single-file wait close to the original 1.5s but shrinks it as the
46
+ file set grows so ``wait * num_files`` cannot overrun the gate timeout (which
47
+ previously caused every diagnostic to be skipped on larger projects).
48
+ """
49
+ if num_files <= 0:
50
+ return _MIN_FILE_WAIT
51
+ return max(_MIN_FILE_WAIT, min(_MAX_FILE_WAIT, settle_budget / num_files))
52
+
53
+
54
+ _LANG_EXTS: Dict[str, tuple] = {
55
+ "python": (".py",),
56
+ "rust": (".rs",),
57
+ "typescript": (".ts", ".tsx"),
58
+ "javascript": (".js", ".jsx", ".mjs", ".cjs"),
59
+ "csharp": (".cs",),
60
+ "kotlin": (".kt", ".kts"),
61
+ "java": (".java",),
62
+ "go": (".go",),
63
+ "ruby": (".rb",),
64
+ }
65
+
66
+ _SKIP_DIRS = frozenset(
67
+ {".venv", "venv", ".git", "node_modules", "__pycache__", "target", "build", "dist"}
68
+ )
69
+
70
+
71
+ def find_source_files(project_root: Path, language: str, cap: int = 40) -> List[str]:
72
+ """Project-relative source paths for ``language`` (bounded by ``cap``)."""
73
+ exts = _LANG_EXTS.get((language or "").lower())
74
+ if not exts:
75
+ return []
76
+ root = Path(project_root)
77
+ out: List[str] = []
78
+ for path in sorted(root.rglob("*")):
79
+ if (
80
+ path.suffix in exts
81
+ and path.is_file()
82
+ and not (_SKIP_DIRS & set(path.parts))
83
+ ):
84
+ out.append(str(path.relative_to(root)))
85
+ if len(out) >= cap:
86
+ break
87
+ return out
88
+
89
+
90
+ def collect_diagnostics(
91
+ project_root: Path,
92
+ language: str,
93
+ rel_files: List[str],
94
+ timeout: float = 30.0,
95
+ ) -> Optional[List[dict]]:
96
+ """Return error-severity diagnostics for ``rel_files``, or None if skipped.
97
+
98
+ None means "no opinion" (unsupported language, multilspy unavailable, or the
99
+ server did not respond within ``timeout``); callers must treat it as a
100
+ no-op, never a pass/fail signal. A list (possibly empty) means the server
101
+ ran: each item is ``{"file", "line", "message"}`` for an error diagnostic.
102
+ """
103
+ code_lang = _LANG_MAP.get((language or "").lower())
104
+ if code_lang is None or not rel_files:
105
+ return None
106
+
107
+ # Bound the in-server settle time to a fraction of the hard timeout so the
108
+ # per-file waits can't sum past it and force a blanket SKIP (the old fixed
109
+ # 1.5s * N did exactly that for ~20+ files at the 30s default).
110
+ settle_budget = max(timeout * 0.7, _MIN_FILE_WAIT)
111
+
112
+ def _work() -> Optional[List[dict]]:
113
+ try:
114
+ return _collect(project_root, code_lang, rel_files, settle_budget)
115
+ except Exception as e: # multilspy/server failures are non-fatal
116
+ logger.debug(f"LSP diagnostics unavailable ({language}): {e}")
117
+ return None
118
+
119
+ # A hung server is abandoned to its daemon thread and the gate skips (None),
120
+ # so the build is never blocked.
121
+ return run_bounded(_work, timeout, None, f"LSP diagnostics ({language})")
122
+
123
+
124
+ def _collect(
125
+ project_root: Path,
126
+ code_lang: str,
127
+ rel_files: List[str],
128
+ settle_budget: float = 60.0,
129
+ ) -> List[dict]:
130
+ import asyncio
131
+
132
+ from multilspy import LanguageServer
133
+ from multilspy.multilspy_config import MultilspyConfig
134
+ from multilspy.multilspy_logger import MultilspyLogger
135
+
136
+ per_file = _per_file_wait(len(rel_files), settle_budget)
137
+
138
+ async def _main() -> List[dict]:
139
+ config = MultilspyConfig.from_dict({"code_language": code_lang})
140
+ server = LanguageServer.create(config, MultilspyLogger(), str(project_root))
141
+ captured: List[dict] = []
142
+ async with server.start_server():
143
+ # multilspy discards publishDiagnostics by default; override the
144
+ # handler to capture them (diagnostics are server-pushed, not a
145
+ # request/response).
146
+ server.server.on_notification(
147
+ "textDocument/publishDiagnostics", captured.append
148
+ )
149
+ for rel in rel_files:
150
+ try:
151
+ with server.open_file(rel):
152
+ await asyncio.sleep(per_file)
153
+ except Exception:
154
+ continue
155
+ return _to_errors(captured)
156
+
157
+ return asyncio.run(_main())
158
+
159
+
160
+ def _to_errors(captured: List[dict]) -> List[dict]:
161
+ errors: List[dict] = []
162
+ for params in captured:
163
+ uri = params.get("uri", "")
164
+ for diag in params.get("diagnostics", []):
165
+ if diag.get("severity") == _LSP_SEVERITY_ERROR:
166
+ line = diag.get("range", {}).get("start", {}).get("line", 0) + 1
167
+ errors.append(
168
+ {
169
+ "file": uri.replace("file://", ""),
170
+ "line": line,
171
+ "message": diag.get("message", ""),
172
+ }
173
+ )
174
+ return errors
@@ -0,0 +1,111 @@
1
+ """Scratchpad learning system ported from /build skill Phase 4.
2
+
3
+ Accumulates discoveries within a build session so later tasks can
4
+ benefit from earlier learnings. Categories match /build:
5
+ env_quirk, pattern, dependency, convention, workaround, pitfall
6
+ """
7
+
8
+ import threading
9
+ from dataclasses import dataclass, field
10
+ from typing import Optional
11
+
12
+
13
+ @dataclass
14
+ class ScratchpadEntry:
15
+ category: str # env_quirk, pattern, dependency, convention, workaround, pitfall
16
+ discovery: str
17
+ task_id: str
18
+ files: list[str] = field(default_factory=list)
19
+ tags: list[str] = field(default_factory=list)
20
+
21
+ def matches(
22
+ self, query_files: list[str] = None, query_tags: list[str] = None
23
+ ) -> bool:
24
+ """Check if this entry is relevant to the given files or tags."""
25
+ if query_files:
26
+ for qf in query_files:
27
+ for ef in self.files:
28
+ if qf in ef or ef in qf:
29
+ return True
30
+ if query_tags:
31
+ for qt in query_tags:
32
+ if qt in self.tags or qt in self.category:
33
+ return True
34
+ return False
35
+
36
+
37
+ class Scratchpad:
38
+ """In-session learning accumulator.
39
+
40
+ Before each task, query for relevant entries. After each task,
41
+ record discoveries. This mirrors /build's scratchpad behavior
42
+ but as a proper Python data structure.
43
+ """
44
+
45
+ def __init__(self):
46
+ self._entries: list[ScratchpadEntry] = []
47
+ self._lock = threading.Lock()
48
+
49
+ def record(
50
+ self,
51
+ category: str,
52
+ discovery: str,
53
+ task_id: str,
54
+ files: Optional[list[str]] = None,
55
+ tags: Optional[list[str]] = None,
56
+ ) -> ScratchpadEntry:
57
+ """Record a discovery from a task execution."""
58
+ entry = ScratchpadEntry(
59
+ category=category,
60
+ discovery=discovery,
61
+ task_id=task_id,
62
+ files=files or [],
63
+ tags=tags or [],
64
+ )
65
+ with self._lock:
66
+ self._entries.append(entry)
67
+ return entry
68
+
69
+ def query(
70
+ self,
71
+ files: Optional[list[str]] = None,
72
+ tags: Optional[list[str]] = None,
73
+ category: Optional[str] = None,
74
+ ) -> list[ScratchpadEntry]:
75
+ """Find relevant entries for a task about to execute."""
76
+ results = []
77
+ with self._lock:
78
+ snapshot = list(self._entries)
79
+ for entry in snapshot:
80
+ if category and entry.category != category:
81
+ continue
82
+ if files or tags:
83
+ if entry.matches(query_files=files, query_tags=tags):
84
+ results.append(entry)
85
+ elif not files and not tags:
86
+ # No filter, return all (optionally filtered by category)
87
+ results.append(entry)
88
+ return results
89
+
90
+ def format_context(
91
+ self,
92
+ files: Optional[list[str]] = None,
93
+ tags: Optional[list[str]] = None,
94
+ max_entries: int = 20,
95
+ ) -> str:
96
+ """Format relevant entries as context string for LLM prompts."""
97
+ entries = self.query(files=files, tags=tags)
98
+ if not entries:
99
+ return ""
100
+ entries = entries[:max_entries]
101
+ lines = ["## Scratchpad (learnings from previous tasks)"]
102
+ for e in entries:
103
+ lines.append(f"- [{e.category}] {e.discovery} (from {e.task_id})")
104
+ return "\n".join(lines)
105
+
106
+ @property
107
+ def entries(self) -> list[ScratchpadEntry]:
108
+ return list(self._entries)
109
+
110
+ def __len__(self) -> int:
111
+ return len(self._entries)
@@ -0,0 +1,43 @@
1
+ """Topological context mapping: a tree-sitter symbol graph over the project.
2
+
3
+ Features:
4
+ - Scope-aware symbols via tree-sitter for Python, Rust, TypeScript/TSX,
5
+ JavaScript/JSX, C, C++, C#, Swift, and Kotlin (best-effort).
6
+ - Per-file outlines (a symbol table of contents) and a whole-project structural
7
+ map, used to navigate and window large files for focused edits.
8
+ - Call-neighbor traversal for related-symbol context, ranked by an optional
9
+ semantic embedder (else lexical).
10
+ - check_syntax(): tree-sitter parse-based syntax verification (ERROR/MISSING
11
+ nodes) that understands strings/comments, used by the correctness gate.
12
+ - Lazy loading; byte-correct slicing for non-ASCII sources.
13
+
14
+ This module was split into cohesive sections (parsers, syntax, nodes, cache,
15
+ graph, engine); every name below is re-exported so the import path
16
+ ``misterdev.core.context.topography`` is unchanged.
17
+ """
18
+
19
+ from ._log import logger
20
+ from .nodes import SymbolNode, _symbol_to_dict, _symbol_from_dict
21
+ from .parsers import _get_ts_parsers, _EXT_TO_LANG, _node_text
22
+ from .syntax import _SYNTAX_CHECK_LANGS, _first_error_node, check_syntax
23
+ from .cache import _CACHE_FORMAT_VERSION, _TopographyCache
24
+ from .graph import _CALL_PATTERN, SymbolGraph
25
+ from .engine import TopographyEngine
26
+
27
+ __all__ = [
28
+ "SymbolNode",
29
+ "SymbolGraph",
30
+ "TopographyEngine",
31
+ "check_syntax",
32
+ "_get_ts_parsers",
33
+ "_TopographyCache",
34
+ "logger",
35
+ "_symbol_to_dict",
36
+ "_symbol_from_dict",
37
+ "_EXT_TO_LANG",
38
+ "_node_text",
39
+ "_SYNTAX_CHECK_LANGS",
40
+ "_first_error_node",
41
+ "_CACHE_FORMAT_VERSION",
42
+ "_CALL_PATTERN",
43
+ ]