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
misterdev/core/modes.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Build mode system ported from /build skill.
|
|
2
|
+
|
|
3
|
+
Determines operating mode from user input and project state.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BuildMode(str, Enum):
|
|
12
|
+
DEBUG = "debug"
|
|
13
|
+
COMPLETE = "complete"
|
|
14
|
+
REVIEW = "review"
|
|
15
|
+
CREATE = "create"
|
|
16
|
+
SPEC = "spec"
|
|
17
|
+
AUTO = "auto"
|
|
18
|
+
SMART = "smart"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BuildFlags:
|
|
22
|
+
"""Parsed flags from user input."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
budget: float = 100.0,
|
|
27
|
+
commit: bool = False,
|
|
28
|
+
no_verify: bool = False,
|
|
29
|
+
no_suggest: bool = False,
|
|
30
|
+
dry_run: bool = False,
|
|
31
|
+
focus: Optional[str] = None,
|
|
32
|
+
interactive: bool = False,
|
|
33
|
+
parallel: bool = False,
|
|
34
|
+
no_rollback: bool = False,
|
|
35
|
+
allow_dirty: bool = False,
|
|
36
|
+
max_tasks: Optional[int] = None,
|
|
37
|
+
):
|
|
38
|
+
self.budget = budget
|
|
39
|
+
self.commit = commit
|
|
40
|
+
self.no_verify = no_verify
|
|
41
|
+
self.no_suggest = no_suggest
|
|
42
|
+
self.dry_run = dry_run
|
|
43
|
+
self.focus = focus
|
|
44
|
+
self.interactive = interactive
|
|
45
|
+
self.parallel = parallel
|
|
46
|
+
self.no_rollback = no_rollback
|
|
47
|
+
self.allow_dirty = allow_dirty
|
|
48
|
+
# Per-run ceiling on decomposed tasks (CLI --max-tasks); None = use the
|
|
49
|
+
# config default. Bounds a focused run so a broad spec can't balloon into
|
|
50
|
+
# dozens of tasks and exhaust the budget.
|
|
51
|
+
self.max_tasks = max_tasks
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return (
|
|
55
|
+
f"BuildFlags(budget={self.budget}, commit={self.commit}, "
|
|
56
|
+
f"dry_run={self.dry_run}, focus={self.focus}, interactive={self.interactive})"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_flags(args: list[str]) -> tuple[list[str], BuildFlags]:
|
|
61
|
+
"""Extract flags from args list, return (remaining_args, flags)."""
|
|
62
|
+
remaining = []
|
|
63
|
+
flags = BuildFlags()
|
|
64
|
+
i = 0
|
|
65
|
+
while i < len(args):
|
|
66
|
+
arg = args[i]
|
|
67
|
+
if arg == "--budget" and i + 1 < len(args):
|
|
68
|
+
flags.budget = float(args[i + 1])
|
|
69
|
+
i += 2
|
|
70
|
+
elif arg == "--commit":
|
|
71
|
+
flags.commit = True
|
|
72
|
+
i += 1
|
|
73
|
+
elif arg == "--no-verify":
|
|
74
|
+
flags.no_verify = True
|
|
75
|
+
i += 1
|
|
76
|
+
elif arg == "--no-suggest":
|
|
77
|
+
flags.no_suggest = True
|
|
78
|
+
i += 1
|
|
79
|
+
elif arg == "--dry-run":
|
|
80
|
+
flags.dry_run = True
|
|
81
|
+
i += 1
|
|
82
|
+
elif arg == "--interactive" or arg == "-i":
|
|
83
|
+
flags.interactive = True
|
|
84
|
+
i += 1
|
|
85
|
+
elif arg == "--parallel":
|
|
86
|
+
flags.parallel = True
|
|
87
|
+
i += 1
|
|
88
|
+
elif arg == "--no-rollback":
|
|
89
|
+
flags.no_rollback = True
|
|
90
|
+
i += 1
|
|
91
|
+
elif arg == "--allow-dirty":
|
|
92
|
+
flags.allow_dirty = True
|
|
93
|
+
i += 1
|
|
94
|
+
elif arg == "--focus" and i + 1 < len(args):
|
|
95
|
+
flags.focus = args[i + 1]
|
|
96
|
+
i += 2
|
|
97
|
+
elif arg == "--max-tasks" and i + 1 < len(args):
|
|
98
|
+
try:
|
|
99
|
+
flags.max_tasks = int(args[i + 1])
|
|
100
|
+
except ValueError:
|
|
101
|
+
flags.max_tasks = None
|
|
102
|
+
i += 2
|
|
103
|
+
else:
|
|
104
|
+
remaining.append(arg)
|
|
105
|
+
i += 1
|
|
106
|
+
return remaining, flags
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_mode(prompt: str, project_path: Path) -> BuildMode:
|
|
110
|
+
"""Determine build mode from prompt content and project state.
|
|
111
|
+
|
|
112
|
+
Resolution order (from /build skill):
|
|
113
|
+
- "debug" -> DEBUG
|
|
114
|
+
- "complete" -> COMPLETE
|
|
115
|
+
- "review" -> REVIEW
|
|
116
|
+
- "new <desc>" -> CREATE
|
|
117
|
+
- path to .md file -> SPEC
|
|
118
|
+
- empty prompt -> AUTO (project exists -> COMPLETE, else CREATE)
|
|
119
|
+
- anything else -> SMART
|
|
120
|
+
"""
|
|
121
|
+
stripped = prompt.strip().lower()
|
|
122
|
+
|
|
123
|
+
if stripped == "debug":
|
|
124
|
+
return BuildMode.DEBUG
|
|
125
|
+
if stripped == "complete":
|
|
126
|
+
return BuildMode.COMPLETE
|
|
127
|
+
if stripped == "review":
|
|
128
|
+
return BuildMode.REVIEW
|
|
129
|
+
if stripped.startswith("new "):
|
|
130
|
+
return BuildMode.CREATE
|
|
131
|
+
|
|
132
|
+
# Check if prompt is a path to an existing .md spec file
|
|
133
|
+
if prompt.strip().endswith(".md"):
|
|
134
|
+
spec_path = project_path / prompt.strip()
|
|
135
|
+
if spec_path.exists():
|
|
136
|
+
return BuildMode.SPEC
|
|
137
|
+
|
|
138
|
+
if not stripped:
|
|
139
|
+
# AUTO: detect from project state
|
|
140
|
+
has_code = _project_has_code(project_path)
|
|
141
|
+
return BuildMode.COMPLETE if has_code else BuildMode.CREATE
|
|
142
|
+
|
|
143
|
+
return BuildMode.SMART
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _project_has_code(project_path: Path) -> bool:
|
|
147
|
+
"""Check if a project directory contains source code files."""
|
|
148
|
+
code_extensions = {
|
|
149
|
+
".py",
|
|
150
|
+
".js",
|
|
151
|
+
".ts",
|
|
152
|
+
".rs",
|
|
153
|
+
".go",
|
|
154
|
+
".java",
|
|
155
|
+
".c",
|
|
156
|
+
".cpp",
|
|
157
|
+
".rb",
|
|
158
|
+
".php",
|
|
159
|
+
".swift",
|
|
160
|
+
".kt",
|
|
161
|
+
".scala",
|
|
162
|
+
".zig",
|
|
163
|
+
}
|
|
164
|
+
for item in project_path.rglob("*"):
|
|
165
|
+
if item.suffix in code_extensions and not _is_venv_or_vendor(item):
|
|
166
|
+
return True
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _is_venv_or_vendor(path: Path) -> bool:
|
|
171
|
+
"""Exclude virtual environments and vendor directories."""
|
|
172
|
+
exclude_dirs = {
|
|
173
|
+
"venv",
|
|
174
|
+
".venv",
|
|
175
|
+
"env",
|
|
176
|
+
"node_modules",
|
|
177
|
+
"vendor",
|
|
178
|
+
"__pycache__",
|
|
179
|
+
".git",
|
|
180
|
+
"target",
|
|
181
|
+
"build",
|
|
182
|
+
"dist",
|
|
183
|
+
}
|
|
184
|
+
return any(part in exclude_dirs for part in path.parts)
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Work recommendation for interactive planning.
|
|
2
|
+
|
|
3
|
+
Given a ProjectAssessment, ask the LLM to propose a short, ranked list of
|
|
4
|
+
concrete work items it would recommend next. Used by the interactive planner
|
|
5
|
+
so the orchestrator composes the plan from the project's live state instead of
|
|
6
|
+
a predefined devplan.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from misterdev.core.planning.assessment import ProjectAssessment
|
|
13
|
+
from misterdev.llm.client import BaseLLMClient
|
|
14
|
+
from misterdev.llm.responses import extract_json_array
|
|
15
|
+
from misterdev.logging_setup import setup_logger
|
|
16
|
+
|
|
17
|
+
logger = setup_logger(__name__)
|
|
18
|
+
|
|
19
|
+
VALID_WORK_TYPES = {"debug", "complete", "feature", "refactor", "test", "docs"}
|
|
20
|
+
|
|
21
|
+
RECOMMEND_PROMPT = """You are advising on what to work on next in this project.
|
|
22
|
+
|
|
23
|
+
Project assessment:
|
|
24
|
+
{summary}
|
|
25
|
+
|
|
26
|
+
Incomplete features: {incomplete}
|
|
27
|
+
Broken/stub code: {broken}
|
|
28
|
+
Open TODO/FIXME: {todos}
|
|
29
|
+
|
|
30
|
+
Propose 3-6 concrete, high-value work items, ranked best-first. Favor fixing
|
|
31
|
+
what is broken and completing what is started over new features. Each item must
|
|
32
|
+
be specific enough to act on (name the area or behavior), not a vague theme.
|
|
33
|
+
|
|
34
|
+
Return ONLY a JSON array, each element:
|
|
35
|
+
{{"title": "...", "rationale": "one sentence why it matters now",
|
|
36
|
+
"work_type": "debug|complete|feature|refactor|test|docs"}}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Recommendation:
|
|
42
|
+
title: str
|
|
43
|
+
rationale: str
|
|
44
|
+
work_type: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def recommend_work(
|
|
48
|
+
assessment: ProjectAssessment, llm_client: BaseLLMClient
|
|
49
|
+
) -> List[Recommendation]:
|
|
50
|
+
"""Return a ranked list of recommended work items, or [] on failure."""
|
|
51
|
+
features = assessment.features
|
|
52
|
+
prompt = RECOMMEND_PROMPT.format(
|
|
53
|
+
summary=assessment.summary(),
|
|
54
|
+
incomplete=_names(getattr(features, "incomplete", [])),
|
|
55
|
+
broken=_names(getattr(features, "broken", [])),
|
|
56
|
+
todos=len(getattr(features, "todos", []) or []),
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
response = llm_client.generate_code(
|
|
60
|
+
prompt, "You are a pragmatic engineering lead. Return only JSON."
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Failed to generate recommendations: {e}")
|
|
64
|
+
return []
|
|
65
|
+
raw = extract_json_array(response)
|
|
66
|
+
|
|
67
|
+
recs: List[Recommendation] = []
|
|
68
|
+
for item in raw:
|
|
69
|
+
if not isinstance(item, dict) or not item.get("title"):
|
|
70
|
+
continue
|
|
71
|
+
wt = str(item.get("work_type", "complete")).lower()
|
|
72
|
+
if wt not in VALID_WORK_TYPES:
|
|
73
|
+
wt = "complete"
|
|
74
|
+
recs.append(
|
|
75
|
+
Recommendation(
|
|
76
|
+
title=str(item["title"]).strip(),
|
|
77
|
+
rationale=str(item.get("rationale", "")).strip(),
|
|
78
|
+
work_type=wt,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return recs
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _names(items) -> str:
|
|
85
|
+
out = []
|
|
86
|
+
for it in items:
|
|
87
|
+
name = getattr(it, "name", None) or getattr(it, "title", None) or str(it)
|
|
88
|
+
out.append(str(name))
|
|
89
|
+
return ", ".join(out[:12]) if out else "none"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Project assessment models ported from /build skill Phase 1.
|
|
2
|
+
|
|
3
|
+
Structured representations of project analysis results used to drive
|
|
4
|
+
all subsequent build phases.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _AssessmentModel(BaseModel):
|
|
12
|
+
"""Base for the assessment models.
|
|
13
|
+
|
|
14
|
+
``validate_assignment`` makes attribute writes (not just construction)
|
|
15
|
+
type-checked. The pipeline mutates these models field-by-field straight from
|
|
16
|
+
LLM JSON (see the analyzer's merge step), so without this a ``null`` or
|
|
17
|
+
wrong-typed write would be stored silently and only crash much later; with it
|
|
18
|
+
a bad write fails fast, at the assignment site.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HealthCheck(_AssessmentModel):
|
|
25
|
+
"""Result of running build, test, and lint commands."""
|
|
26
|
+
|
|
27
|
+
builds: bool = False
|
|
28
|
+
build_output: str = ""
|
|
29
|
+
tests_pass: bool = False
|
|
30
|
+
test_count: int = 0
|
|
31
|
+
test_failures: int = 0
|
|
32
|
+
test_output: str = ""
|
|
33
|
+
lint_clean: bool = False
|
|
34
|
+
lint_warnings: int = 0
|
|
35
|
+
lint_output: str = ""
|
|
36
|
+
missing_dependencies: list[str] = Field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FeatureInfo(_AssessmentModel):
|
|
40
|
+
"""A single feature with evidence of its state."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
description: str = ""
|
|
44
|
+
evidence_files: list[str] = Field(default_factory=list)
|
|
45
|
+
complexity: str = "medium" # trivial, small, medium, large, architectural
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FeatureInventory(_AssessmentModel):
|
|
49
|
+
"""Completeness analysis from /build Phase 1b."""
|
|
50
|
+
|
|
51
|
+
existing: list[FeatureInfo] = Field(default_factory=list)
|
|
52
|
+
incomplete: list[FeatureInfo] = Field(default_factory=list)
|
|
53
|
+
missing: list[FeatureInfo] = Field(default_factory=list)
|
|
54
|
+
dead_code: list[str] = Field(default_factory=list)
|
|
55
|
+
stubs: list[str] = Field(default_factory=list)
|
|
56
|
+
broken: list[str] = Field(default_factory=list)
|
|
57
|
+
todos: list[dict] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ProjectStructure(_AssessmentModel):
|
|
61
|
+
"""Structural profile from /build Phase 1a."""
|
|
62
|
+
|
|
63
|
+
project_type: str = "unknown" # web-api, web-app, cli, library, etc.
|
|
64
|
+
languages: list[str] = Field(default_factory=list)
|
|
65
|
+
frameworks: list[str] = Field(default_factory=list)
|
|
66
|
+
build_system: Optional[str] = None
|
|
67
|
+
build_command: Optional[str] = None
|
|
68
|
+
test_system: Optional[str] = None
|
|
69
|
+
test_command: Optional[str] = None
|
|
70
|
+
lint_command: Optional[str] = None
|
|
71
|
+
package_manager: Optional[str] = None
|
|
72
|
+
entry_points: list[str] = Field(default_factory=list)
|
|
73
|
+
directory_structure: str = ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ProjectContext(_AssessmentModel):
|
|
77
|
+
"""Contextual information from /build Phase 1c."""
|
|
78
|
+
|
|
79
|
+
purpose: str = ""
|
|
80
|
+
goals: str = ""
|
|
81
|
+
conventions: str = ""
|
|
82
|
+
constraints: str = ""
|
|
83
|
+
recent_activity: str = ""
|
|
84
|
+
stated_requirements: str = ""
|
|
85
|
+
reference_impl: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TechnicalDebt(_AssessmentModel):
|
|
89
|
+
"""Technical debt estimation from /build Phase 1."""
|
|
90
|
+
|
|
91
|
+
score: int = 0 # 0-100
|
|
92
|
+
description: str = ""
|
|
93
|
+
critical_issues: list[str] = Field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RiskAssessment(_AssessmentModel):
|
|
97
|
+
"""Risk analysis for the proposed build."""
|
|
98
|
+
|
|
99
|
+
level: str = "low" # low, medium, high, critical
|
|
100
|
+
factors: list[str] = Field(default_factory=list)
|
|
101
|
+
mitigations: list[str] = Field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ProjectAssessment(_AssessmentModel):
|
|
105
|
+
"""Merged assessment from all Phase 1 agents.
|
|
106
|
+
|
|
107
|
+
This is the central data structure that drives Phases 2-6.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
structure: ProjectStructure = Field(default_factory=ProjectStructure)
|
|
111
|
+
health: HealthCheck = Field(default_factory=HealthCheck)
|
|
112
|
+
features: FeatureInventory = Field(default_factory=FeatureInventory)
|
|
113
|
+
context: ProjectContext = Field(default_factory=ProjectContext)
|
|
114
|
+
tech_debt: TechnicalDebt = Field(default_factory=TechnicalDebt)
|
|
115
|
+
risk: RiskAssessment = Field(default_factory=RiskAssessment)
|
|
116
|
+
|
|
117
|
+
def summary(self) -> str:
|
|
118
|
+
"""One-line summary for logging."""
|
|
119
|
+
s = self.structure
|
|
120
|
+
h = self.health
|
|
121
|
+
lang = ", ".join(s.languages) if s.languages else "unknown"
|
|
122
|
+
build_status = "OK" if h.builds else "FAIL"
|
|
123
|
+
# Clamp the passing count: some runner parsers fill test_count and
|
|
124
|
+
# test_failures from independent regexes, so a malformed log can yield
|
|
125
|
+
# failures > count, which would otherwise render a negative "passed".
|
|
126
|
+
test_status = (
|
|
127
|
+
f"{max(0, h.test_count - h.test_failures)}/{h.test_count}"
|
|
128
|
+
if h.test_count
|
|
129
|
+
else "none"
|
|
130
|
+
)
|
|
131
|
+
return (
|
|
132
|
+
f"[{s.project_type}] {lang} | "
|
|
133
|
+
f"build={build_status} tests={test_status} "
|
|
134
|
+
f"lint_warnings={h.lint_warnings}"
|
|
135
|
+
)
|