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,137 @@
1
+ """Coercion helpers and mergers that fold analyzer JSON into the assessment."""
2
+
3
+ from misterdev.core.planning.assessment import (
4
+ FeatureInfo,
5
+ ProjectAssessment,
6
+ )
7
+
8
+
9
+ def _health_ground_truth(health) -> str:
10
+ """One-line verified-state preamble to anchor the completeness analyzer."""
11
+ build = "passes" if health.builds else "FAILS"
12
+ if health.test_count:
13
+ passing = health.test_count - health.test_failures
14
+ tests = f"{passing}/{health.test_count} tests passing"
15
+ elif health.tests_pass:
16
+ tests = "test suite passes"
17
+ else:
18
+ tests = "no test results"
19
+ return f"VERIFIED ground truth: build {build}; {tests}."
20
+
21
+
22
+ def _as_str_list(value, default: list) -> list:
23
+ """Coerce an LLM-provided value to a ``list[str]``, else return ``default``.
24
+
25
+ The analyzers occasionally emit ``null`` (or a bare string) for a field the
26
+ schema types as a list. ``dict.get(k, default)`` only falls back when the key
27
+ is ABSENT, so a present ``null`` would store ``None`` on the typed field
28
+ (Pydantic does not validate on assignment) and later crash a ``for``/``join``
29
+ over it. Normalizing here keeps the trust boundary sound.
30
+ """
31
+ if isinstance(value, list):
32
+ return [v for v in value if isinstance(v, str)]
33
+ return default
34
+
35
+
36
+ def _as_int(value, default: int) -> int:
37
+ """Coerce an LLM-provided value to ``int``, else return ``default``.
38
+
39
+ Tolerates a numeric string ("75") or float (75.0); rejects bools, ``null``,
40
+ and unparseable text so a typed ``int`` field never holds a string.
41
+ """
42
+ if isinstance(value, bool):
43
+ return default
44
+ if isinstance(value, (int, float)):
45
+ return int(value)
46
+ if isinstance(value, str):
47
+ try:
48
+ return int(value.strip())
49
+ except ValueError:
50
+ return default
51
+ return default
52
+
53
+
54
+ def _merge_structure(assessment: ProjectAssessment, data: dict) -> None:
55
+ if not data:
56
+ return
57
+ s = assessment.structure
58
+ s.project_type = data.get("project_type") or s.project_type
59
+ s.languages = _as_str_list(data.get("languages"), s.languages)
60
+ s.frameworks = _as_str_list(data.get("frameworks"), s.frameworks)
61
+ s.build_command = data.get("build_command", s.build_command)
62
+ s.test_command = data.get("test_command", s.test_command)
63
+ s.lint_command = data.get("lint_command", s.lint_command)
64
+ s.package_manager = data.get("package_manager", s.package_manager)
65
+ s.entry_points = _as_str_list(data.get("entry_points"), s.entry_points)
66
+ s.directory_structure = data.get("directory_structure") or s.directory_structure
67
+
68
+
69
+ def _merge_completeness(assessment: ProjectAssessment, data: dict) -> None:
70
+ if not data:
71
+ return
72
+ f = assessment.features
73
+ for item in data.get("existing") or []:
74
+ if isinstance(item, dict):
75
+ f.existing.append(
76
+ FeatureInfo(
77
+ name=item.get("name", ""), description=item.get("description", "")
78
+ )
79
+ )
80
+ for item in data.get("incomplete") or []:
81
+ if isinstance(item, dict):
82
+ f.incomplete.append(
83
+ FeatureInfo(
84
+ name=item.get("name", ""),
85
+ description=item.get("description", ""),
86
+ complexity=item.get("complexity", "medium"),
87
+ )
88
+ )
89
+ for item in data.get("missing") or []:
90
+ if isinstance(item, dict):
91
+ f.missing.append(
92
+ FeatureInfo(
93
+ name=item.get("name", ""),
94
+ description=item.get("description", ""),
95
+ complexity=item.get("complexity", "medium"),
96
+ )
97
+ )
98
+ f.dead_code = _as_str_list(data.get("dead_code"), f.dead_code)
99
+ f.stubs = _as_str_list(data.get("stubs"), f.stubs)
100
+ f.broken = _as_str_list(data.get("broken"), f.broken)
101
+ todos = data.get("todos")
102
+ f.todos = todos if isinstance(todos, list) else f.todos
103
+
104
+
105
+ def _merge_context(assessment: ProjectAssessment, data: dict) -> None:
106
+ if not data:
107
+ return
108
+ c = assessment.context
109
+ c.purpose = data.get("purpose", c.purpose)
110
+ c.goals = data.get("goals", c.goals)
111
+ c.conventions = data.get("conventions", c.conventions)
112
+ c.constraints = data.get("constraints", c.constraints)
113
+ c.recent_activity = data.get("recent_activity", c.recent_activity)
114
+ c.stated_requirements = data.get("stated_requirements", c.stated_requirements)
115
+
116
+
117
+ def _merge_debt_risk(assessment: ProjectAssessment, data: dict) -> None:
118
+ if not data:
119
+ return
120
+
121
+ debt_data = data.get("tech_debt") or {}
122
+ if debt_data:
123
+ assessment.tech_debt.score = _as_int(debt_data.get("score"), 0)
124
+ assessment.tech_debt.description = debt_data.get("description") or ""
125
+ assessment.tech_debt.critical_issues = _as_str_list(
126
+ debt_data.get("critical_issues"), assessment.tech_debt.critical_issues
127
+ )
128
+
129
+ risk_data = data.get("risk") or {}
130
+ if risk_data:
131
+ assessment.risk.level = risk_data.get("level") or "low"
132
+ assessment.risk.factors = _as_str_list(
133
+ risk_data.get("factors"), assessment.risk.factors
134
+ )
135
+ assessment.risk.mitigations = _as_str_list(
136
+ risk_data.get("mitigations"), assessment.risk.mitigations
137
+ )
@@ -0,0 +1,312 @@
1
+ """Raw project gathering: file listing, config/doc reads, source overview, git log."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from misterdev.core.gitcmd import run_git
8
+ from misterdev.logging_setup import setup_logger
9
+
10
+ logger = setup_logger(__name__)
11
+
12
+
13
+ _IGNORE_DIRS = {
14
+ "venv",
15
+ ".venv",
16
+ "node_modules",
17
+ "__pycache__",
18
+ ".git",
19
+ "target",
20
+ "build",
21
+ "dist",
22
+ ".tox",
23
+ ".mypy_cache",
24
+ ".ruff_cache",
25
+ ".pytest_cache",
26
+ ".eggs",
27
+ "vendor",
28
+ }
29
+
30
+
31
+ def _walk_limited(root: Path, max_depth: int = 6):
32
+ """Yield files under root, pruning ignored dirs and bounding depth.
33
+
34
+ Unlike Path.rglob, this prunes large directories (node_modules, target)
35
+ during traversal instead of descending into them and filtering afterward,
36
+ so scanning a monorepo does not stall on vendored trees.
37
+ """
38
+ stack = [(root, 0)]
39
+ while stack:
40
+ directory, depth = stack.pop()
41
+ try:
42
+ entries = sorted(directory.iterdir())
43
+ except (PermissionError, OSError):
44
+ continue
45
+ for entry in entries:
46
+ if entry.is_dir():
47
+ if entry.name in _IGNORE_DIRS or entry.name.startswith("."):
48
+ continue
49
+ if depth < max_depth:
50
+ stack.append((entry, depth + 1))
51
+ elif entry.is_file():
52
+ yield entry
53
+
54
+
55
+ def _get_file_listing(
56
+ project_path: Path, max_files: int = 200, max_depth: int = 6
57
+ ) -> str:
58
+ """Get a truncated file listing for the project."""
59
+ files = []
60
+ for item in _walk_limited(project_path, max_depth):
61
+ rel = item.relative_to(project_path)
62
+ files.append(str(rel))
63
+ if len(files) >= max_files:
64
+ files.append(f"... ({max_files}+ files, truncated)")
65
+ break
66
+ return "\n".join(sorted(files))
67
+
68
+
69
+ def _read_config_files(project_path: Path) -> str:
70
+ """Read common config files for structure analysis."""
71
+ config_names = [
72
+ "pyproject.toml",
73
+ "setup.py",
74
+ "setup.cfg",
75
+ "package.json",
76
+ "Cargo.toml",
77
+ "go.mod",
78
+ "Makefile",
79
+ "CMakeLists.txt",
80
+ "project.yaml",
81
+ "tsconfig.json",
82
+ "webpack.config.js",
83
+ ]
84
+ contents = []
85
+ for name in config_names:
86
+ text = _read_file_safe(project_path / name, max_lines=100)
87
+ if text:
88
+ contents.append(f"### {name}\n{text}")
89
+ return "\n\n".join(contents) if contents else "(no config files found)"
90
+
91
+
92
+ def _read_docs(project_path: Path) -> str:
93
+ """Read documentation files."""
94
+ doc_names = ["README.md", "CLAUDE.md", "SPEC.md", "DESIGN.md", "REQUIREMENTS.md"]
95
+ contents = []
96
+ for name in doc_names:
97
+ text = _read_file_safe(project_path / name, max_lines=200)
98
+ if text:
99
+ contents.append(f"### {name}\n{text}")
100
+ return "\n\n".join(contents) if contents else "(no docs found)"
101
+
102
+
103
+ _OVERVIEW_CODE_EXTS = {
104
+ ".py",
105
+ ".js",
106
+ ".jsx",
107
+ ".ts",
108
+ ".tsx",
109
+ ".rs",
110
+ ".go",
111
+ ".java",
112
+ ".c",
113
+ ".h",
114
+ ".cpp",
115
+ ".cc",
116
+ ".cxx",
117
+ ".hpp",
118
+ ".hh",
119
+ ".swift",
120
+ ".cs",
121
+ ".kt",
122
+ }
123
+
124
+
125
+ # Phrases that mark an empty/default/no-op result as deliberate rather than
126
+ # unfinished. When a file's leading doc contains one, the sentence carrying it is
127
+ # preserved into the overview so the completeness analyzer does not flag the code
128
+ # as a stub.
129
+ _INTENT_KEYWORDS = (
130
+ "degrade",
131
+ "no-op",
132
+ "noop",
133
+ "fallback",
134
+ "parity",
135
+ "by design",
136
+ "intentional",
137
+ "graceful",
138
+ "historical",
139
+ "never panic",
140
+ )
141
+
142
+
143
+ def _leading_doc(path: Path, max_lines: int = 40, max_chars: int = 220) -> str:
144
+ """Extract a source file's leading comment/doc block as one compact line.
145
+
146
+ Carries each file's stated *intent* (e.g. "degrades to empty on wasm — the
147
+ historical missing-model contract") into completeness analysis, so a deep
148
+ file is judged by its documented purpose rather than guessed from its symbol
149
+ names. Returns "" when the file opens with code and no leading comment.
150
+ """
151
+ try:
152
+ with path.open("r", encoding="utf-8", errors="replace") as fh:
153
+ head = [next(fh, "") for _ in range(max_lines)]
154
+ except OSError:
155
+ return ""
156
+
157
+ # `#` is a line comment in Python but a preprocessor directive in C-family
158
+ # files, so only treat it (and `"""` docstrings) as a comment for Python.
159
+ is_py = path.suffix == ".py"
160
+ line_prefixes = ("///", "//!", "//", "#") if is_py else ("///", "//!", "//")
161
+ doc: list[str] = []
162
+ in_block = False
163
+ block_end = ""
164
+ for raw in head:
165
+ line = raw.strip()
166
+ if not in_block:
167
+ if not line:
168
+ # Skip blank lines anywhere in the leading comment region (not just
169
+ # before the first comment), so an intent stated after a blank
170
+ # separator line is still collected; the first real CODE line below
171
+ # still ends the block.
172
+ continue
173
+ if not doc and line.startswith("#!"):
174
+ continue # shebang
175
+ if in_block:
176
+ end = line.find(block_end)
177
+ seg = (line[:end] if end != -1 else line).lstrip("*").strip()
178
+ if seg:
179
+ doc.append(seg)
180
+ if end != -1:
181
+ break
182
+ continue
183
+ if is_py and (line.startswith('"""') or line.startswith("'''")):
184
+ quote = line[:3]
185
+ rest = line[3:].rstrip()
186
+ if rest.endswith(quote) and len(rest) >= 3:
187
+ doc.append(rest[:-3].strip())
188
+ break
189
+ in_block, block_end = True, quote
190
+ if rest.strip():
191
+ doc.append(rest.strip())
192
+ continue
193
+ if line.startswith("/*"):
194
+ rest = line[2:]
195
+ if "*/" in rest:
196
+ doc.append(rest.split("*/", 1)[0].strip())
197
+ break
198
+ in_block, block_end = True, "*/"
199
+ seg = rest.lstrip("*").strip()
200
+ if seg:
201
+ doc.append(seg)
202
+ continue
203
+ matched = next((p for p in line_prefixes if line.startswith(p)), None)
204
+ if matched is not None:
205
+ doc.append(line[len(matched) :].strip())
206
+ continue
207
+ break # first line of real code ends the leading doc block
208
+
209
+ full = " ".join(d for d in doc if d).strip()
210
+ if not full:
211
+ return ""
212
+ summary = full[:max_chars]
213
+ # The strongest "this empty/no-op is intentional" signal often sits a few
214
+ # sentences into a module doc, past the summary cap. If the block states such
215
+ # intent, graft the sentence that says so onto the summary so it is never
216
+ # truncated away — this is exactly what stops a documented graceful-degrade
217
+ # backend from being misread as an unfinished stub.
218
+ lowered = full.lower()
219
+ if any(k in lowered for k in _INTENT_KEYWORDS):
220
+ for sentence in re.split(r"(?<=[.;])\s+", full):
221
+ sl = sentence.lower()
222
+ if any(k in sl for k in _INTENT_KEYWORDS) and sentence not in summary:
223
+ summary = f"{summary.rstrip()} … {sentence.strip()}"[: max_chars + 180]
224
+ break
225
+ return summary
226
+
227
+
228
+ def _get_source_overview(
229
+ project_path: Path, max_chars: int = 8000, outline: Optional[str] = None
230
+ ) -> str:
231
+ """Whole-project structural map plus the heads of source files.
232
+
233
+ The symbol map (every file and its symbols, from tree-sitter) conveys the
234
+ architecture densely so planning is grounded in the entire project rather
235
+ than the first few files that fit ``max_chars`` of raw heads. A per-file
236
+ intent map (leading doc comments) then carries each file's documented purpose
237
+ — including deliberate graceful-degradation — so deep files are not judged by
238
+ their symbol names alone.
239
+
240
+ ``outline`` lets the caller pass an already-built symbol outline (the
241
+ project's TopographyEngine graph). When given, this reuses it instead of
242
+ parsing a second throwaway ``SymbolGraph`` here, so the whole-project graph is
243
+ built once per run rather than once for the overview AND once for the engine.
244
+ """
245
+ parts = []
246
+ if outline is None:
247
+ try:
248
+ from misterdev.core.context.topography import SymbolGraph
249
+
250
+ graph = SymbolGraph(project_path)
251
+ graph.build()
252
+ outline = graph.project_outline()
253
+ except (ImportError, OSError, ValueError) as e:
254
+ logger.debug(f"symbol-based overview unavailable: {e}")
255
+ outline = ""
256
+ if outline:
257
+ parts.append("## Project structure (files and symbols)\n" + outline)
258
+
259
+ intents = []
260
+ intent_total = 0
261
+ for item in _walk_limited(project_path):
262
+ if item.suffix in _OVERVIEW_CODE_EXTS:
263
+ doc = _leading_doc(item)
264
+ if doc:
265
+ line = f"{item.relative_to(project_path)}: {doc}"
266
+ intents.append(line)
267
+ intent_total += len(line) + 1
268
+ if intent_total >= 16000:
269
+ break
270
+ if intents:
271
+ parts.append(
272
+ "## File intents (leading doc comments)\n"
273
+ "Documented graceful-degradation, platform-gated no-ops, and parity "
274
+ "shims are intentional and COMPLETE — not stubs.\n" + "\n".join(intents)
275
+ )
276
+
277
+ head = []
278
+ total = 0
279
+ for item in _walk_limited(project_path):
280
+ if item.suffix in _OVERVIEW_CODE_EXTS:
281
+ text = _read_file_safe(item, max_lines=30)
282
+ if text:
283
+ rel = item.relative_to(project_path)
284
+ chunk = f"### {rel}\n{text}\n"
285
+ head.append(chunk)
286
+ total += len(chunk)
287
+ if total >= max_chars:
288
+ break
289
+ if head:
290
+ parts.append("## Source heads\n" + "\n".join(head))
291
+ return "\n\n".join(parts) if parts else "(no source files found)"
292
+
293
+
294
+ def _get_git_log(project_path: Path, count: int = 20) -> str:
295
+ """Get recent git log."""
296
+ proc = run_git(f"git log --oneline -n {count}", project_path, timeout=10)
297
+ if proc is None:
298
+ return "(git log unavailable)"
299
+ return proc.stdout.strip() if proc.returncode == 0 else "(not a git repo)"
300
+
301
+
302
+ def _read_file_safe(path: Path, max_lines: int = 500) -> str:
303
+ """Read a file, returning empty string on failure."""
304
+ try:
305
+ if not path.exists():
306
+ return ""
307
+ lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
308
+ if len(lines) > max_lines:
309
+ lines = lines[:max_lines] + [f"... ({len(lines)} lines total, truncated)"]
310
+ return "\n".join(lines)
311
+ except Exception:
312
+ return ""
@@ -0,0 +1,83 @@
1
+ """Prompt templates for the Phase 1 project analyzers."""
2
+
3
+ STRUCTURE_PROMPT = """Analyze the project at the path below. Return a JSON object with these exact fields:
4
+ project_type (string: web-api, web-app, cli, library, worker, static-site, monorepo, or unknown),
5
+ languages (array of strings),
6
+ frameworks (array of strings),
7
+ build_command (string or null),
8
+ test_command (string or null),
9
+ lint_command (string or null),
10
+ package_manager (string or null),
11
+ entry_points (array of file path strings),
12
+ directory_structure (string, tree of src dirs, max 30 lines)
13
+
14
+ Project files:
15
+ {file_listing}
16
+
17
+ Key config files:
18
+ {config_contents}
19
+
20
+ Return ONLY valid JSON, no markdown fences."""
21
+
22
+ COMPLETENESS_PROMPT = """Analyze this project for completeness. Return a JSON object with:
23
+ existing (array of objects with "name" and "description"),
24
+ incomplete (array of objects with "name", "description", and "complexity"),
25
+ missing (array of objects with "name", "description", and "complexity"),
26
+ dead_code (array of file path strings),
27
+ stubs (array of file path strings),
28
+ broken (array of file path strings),
29
+ todos (array of objects with "file", "line", "text")
30
+
31
+ {health_ground}
32
+ Treat code that builds and is covered by passing tests as IMPLEMENTED. Docs may
33
+ describe a from-scratch plan; do NOT report already-built, tested capabilities
34
+ as missing or incomplete. Base "missing"/"broken" on the source, not the docs.
35
+
36
+ Code documented as intentional is COMPLETE, not incomplete: graceful-degradation
37
+ and fallback paths, platform-gated no-ops (e.g. a wasm/no-filesystem backend that
38
+ returns empty by design), and shims "retained for parity". A function returning an
39
+ empty or default value is NOT a stub when a comment or the design states that empty
40
+ result is the contract — check the "File intents" section before flagging. For each
41
+ item you place in "incomplete"/"stubs"/"missing", name the specific file and the
42
+ concrete unmet behavior; if you cannot, omit it. Never infer incompleteness from a
43
+ symbol name or a default return alone.
44
+
45
+ Project docs:
46
+ {docs}
47
+
48
+ Project source overview:
49
+ {source_overview}
50
+
51
+ Return ONLY valid JSON, no markdown fences."""
52
+
53
+ CONTEXT_PROMPT = """Gather context for this project. Return a JSON object with:
54
+ purpose (string, 1-2 sentences),
55
+ goals (string),
56
+ conventions (string, coding conventions found),
57
+ constraints (string, requirements or compatibility needs),
58
+ recent_activity (string, summary of recent work),
59
+ stated_requirements (string, from spec/design docs)
60
+
61
+ README:
62
+ {readme}
63
+
64
+ CLAUDE.md / config:
65
+ {config}
66
+
67
+ Recent git log:
68
+ {git_log}
69
+
70
+ Return ONLY valid JSON, no markdown fences."""
71
+
72
+
73
+ DEBT_RISK_PROMPT = """Analyze this project for technical debt and implementation risk. Return a JSON object with:
74
+ tech_debt (object with "score" [0-100], "description", "critical_issues"),
75
+ risk (object with "level" [low, medium, high, critical], "factors", "mitigations")
76
+
77
+ Project assessment so far:
78
+ {assessment_summary}
79
+
80
+ Source code overview:
81
+ {source_overview}
82
+
83
+ Return ONLY valid JSON, no markdown fences."""