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,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."""
|