aria-code 4.1.3__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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
workspace/files.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Safe workspace file operations for Aria Code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
import re
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ReadResult:
|
|
14
|
+
path: str
|
|
15
|
+
lines: int
|
|
16
|
+
content: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkspaceSecurity:
|
|
20
|
+
"""Path security policy shared by CLI tools and future runtimes."""
|
|
21
|
+
|
|
22
|
+
BLOCKED_ROOTS = (
|
|
23
|
+
"/etc",
|
|
24
|
+
"/sys",
|
|
25
|
+
"/proc",
|
|
26
|
+
"/dev",
|
|
27
|
+
"/boot",
|
|
28
|
+
"/root",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __init__(self, cwd: str | pathlib.Path | None = None) -> None:
|
|
32
|
+
self.cwd = pathlib.Path(cwd or pathlib.Path.cwd()).expanduser().resolve()
|
|
33
|
+
|
|
34
|
+
def allowed_roots(self) -> List[pathlib.Path]:
|
|
35
|
+
roots = [pathlib.Path.home().resolve(), pathlib.Path("/tmp").resolve(), self.cwd]
|
|
36
|
+
for tmp_candidate in (
|
|
37
|
+
"/var/folders",
|
|
38
|
+
"/private/tmp",
|
|
39
|
+
"/private/var/folders",
|
|
40
|
+
"/var/tmp",
|
|
41
|
+
"/private/var/tmp",
|
|
42
|
+
tempfile.gettempdir(),
|
|
43
|
+
):
|
|
44
|
+
try:
|
|
45
|
+
root = pathlib.Path(tmp_candidate).resolve()
|
|
46
|
+
if root not in roots:
|
|
47
|
+
roots.append(root)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return roots
|
|
51
|
+
|
|
52
|
+
def resolve(self, path: str | pathlib.Path) -> pathlib.Path:
|
|
53
|
+
return pathlib.Path(path).expanduser().resolve()
|
|
54
|
+
|
|
55
|
+
def is_safe_path(self, path: str | pathlib.Path) -> bool:
|
|
56
|
+
resolved = self.resolve(path)
|
|
57
|
+
for blocked in self.BLOCKED_ROOTS:
|
|
58
|
+
try:
|
|
59
|
+
resolved.relative_to(pathlib.Path(blocked).resolve())
|
|
60
|
+
return False
|
|
61
|
+
except ValueError:
|
|
62
|
+
pass
|
|
63
|
+
for root in self.allowed_roots():
|
|
64
|
+
try:
|
|
65
|
+
resolved.relative_to(root)
|
|
66
|
+
return True
|
|
67
|
+
except ValueError:
|
|
68
|
+
pass
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def require_safe(self, path: str | pathlib.Path) -> pathlib.Path:
|
|
72
|
+
resolved = self.resolve(path)
|
|
73
|
+
if not self.is_safe_path(resolved):
|
|
74
|
+
raise PermissionError(f"Access denied: path '{resolved}' is outside allowed directories")
|
|
75
|
+
return resolved
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class WorkspaceFiles:
|
|
79
|
+
"""Read, list, and search files under Aria Code's safety policy."""
|
|
80
|
+
|
|
81
|
+
MAX_READ_BYTES = 2_000_000
|
|
82
|
+
LARGE_FILE_BYTES = 500_000
|
|
83
|
+
LARGE_FILE_DEFAULT_LINES = 500
|
|
84
|
+
MAX_SEARCH_BYTES = 5_000_000
|
|
85
|
+
|
|
86
|
+
def __init__(self, security: WorkspaceSecurity | None = None) -> None:
|
|
87
|
+
self.security = security or WorkspaceSecurity()
|
|
88
|
+
|
|
89
|
+
def read_file(self, path: str, offset: int = 0, limit: int = 0) -> ReadResult:
|
|
90
|
+
target = self.security.require_safe(path)
|
|
91
|
+
if not target.exists():
|
|
92
|
+
raise FileNotFoundError(f"File not found: {target}")
|
|
93
|
+
if not target.is_file():
|
|
94
|
+
raise ValueError(f"Not a file: {target}")
|
|
95
|
+
size = target.stat().st_size
|
|
96
|
+
if size > self.MAX_READ_BYTES:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"File too large: {size:,} bytes (max 2 MB). "
|
|
99
|
+
"Use offset/limit parameters to read sections."
|
|
100
|
+
)
|
|
101
|
+
text = target.read_text(errors="replace")
|
|
102
|
+
lines = text.splitlines()
|
|
103
|
+
total_lines = len(lines)
|
|
104
|
+
if not offset and not limit and size > self.LARGE_FILE_BYTES:
|
|
105
|
+
limit = self.LARGE_FILE_DEFAULT_LINES
|
|
106
|
+
if offset or limit:
|
|
107
|
+
end = offset + limit if limit else total_lines
|
|
108
|
+
shown = lines[offset:end]
|
|
109
|
+
content = "\n".join(f"{i + offset + 1:4d}│ {line}" for i, line in enumerate(shown))
|
|
110
|
+
if limit and end < total_lines:
|
|
111
|
+
content += f"\n... [{len(shown)} of {total_lines} lines shown — use offset/limit to read more]"
|
|
112
|
+
return ReadResult(str(target), len(shown), content[:30000])
|
|
113
|
+
content = "\n".join(f"{i + 1:4d}│ {line}" for i, line in enumerate(lines))
|
|
114
|
+
return ReadResult(str(target), len(lines), content[:30000])
|
|
115
|
+
|
|
116
|
+
def list_files(self, path: str = ".", pattern: str = "*") -> Dict[str, Any]:
|
|
117
|
+
target = self.security.require_safe(path)
|
|
118
|
+
if not target.exists():
|
|
119
|
+
raise FileNotFoundError(f"Path not found: {target}")
|
|
120
|
+
if target.is_file():
|
|
121
|
+
read = self.read_file(str(target))
|
|
122
|
+
return {
|
|
123
|
+
"path": read.path,
|
|
124
|
+
"pattern": pattern,
|
|
125
|
+
"count": 1,
|
|
126
|
+
"items": [{"name": target.name, "type": "file", "size": target.stat().st_size}],
|
|
127
|
+
"content": read.content,
|
|
128
|
+
}
|
|
129
|
+
matches = sorted(target.glob(pattern))[:100]
|
|
130
|
+
items = []
|
|
131
|
+
for match in matches:
|
|
132
|
+
try:
|
|
133
|
+
self.security.require_safe(match)
|
|
134
|
+
except PermissionError:
|
|
135
|
+
continue
|
|
136
|
+
rel = match.relative_to(target) if match.is_relative_to(target) else match
|
|
137
|
+
kind = "dir" if match.is_dir() else "file"
|
|
138
|
+
size = match.stat().st_size if match.is_file() else 0
|
|
139
|
+
items.append({"name": str(rel), "type": kind, "size": size})
|
|
140
|
+
return {"path": str(target), "pattern": pattern, "count": len(items), "items": items}
|
|
141
|
+
|
|
142
|
+
def search_code(self, pattern: str, path: str = ".", file_glob: str = "**/*.py") -> Dict[str, Any]:
|
|
143
|
+
if not pattern:
|
|
144
|
+
raise ValueError("Missing 'pattern' parameter")
|
|
145
|
+
target = self.security.require_safe(path)
|
|
146
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
147
|
+
matches = []
|
|
148
|
+
for file_path in sorted(target.glob(file_glob))[:200]:
|
|
149
|
+
try:
|
|
150
|
+
safe_file = self.security.require_safe(file_path)
|
|
151
|
+
except PermissionError:
|
|
152
|
+
continue
|
|
153
|
+
if not safe_file.is_file() or safe_file.stat().st_size > self.MAX_SEARCH_BYTES:
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
lines = safe_file.read_text(errors="replace").splitlines()
|
|
157
|
+
except Exception:
|
|
158
|
+
continue
|
|
159
|
+
for line_number, line in enumerate(lines, 1):
|
|
160
|
+
if regex.search(line):
|
|
161
|
+
matches.append({
|
|
162
|
+
"file": str(safe_file.relative_to(target) if safe_file.is_relative_to(target) else safe_file),
|
|
163
|
+
"line": line_number,
|
|
164
|
+
"content": line.strip()[:200],
|
|
165
|
+
})
|
|
166
|
+
if len(matches) >= 50:
|
|
167
|
+
break
|
|
168
|
+
if len(matches) >= 50:
|
|
169
|
+
break
|
|
170
|
+
return {"pattern": pattern, "path": str(target), "count": len(matches), "matches": matches}
|
workspace/verify.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Verification planning for Aria Code workspaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Iterable, List
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class VerificationPlan:
|
|
15
|
+
commands: List[str]
|
|
16
|
+
reason: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VerificationPlanner:
|
|
20
|
+
"""Infer focused verification commands from changed files and project files."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, root: str | pathlib.Path = ".") -> None:
|
|
23
|
+
self.root = pathlib.Path(root).expanduser().resolve()
|
|
24
|
+
|
|
25
|
+
def infer(self, paths: Iterable[str] | None = None) -> VerificationPlan:
|
|
26
|
+
changed = [p for p in (paths or []) if p]
|
|
27
|
+
if not changed:
|
|
28
|
+
changed = self._git_changed_files()
|
|
29
|
+
commands: List[str] = []
|
|
30
|
+
reasons: List[str] = []
|
|
31
|
+
|
|
32
|
+
py_files = [p for p in changed if p.endswith(".py")]
|
|
33
|
+
if py_files:
|
|
34
|
+
quoted = " ".join(shlex.quote(p) for p in py_files[:20])
|
|
35
|
+
commands.append(f"python3 -m py_compile {quoted}")
|
|
36
|
+
reasons.append("Python files changed")
|
|
37
|
+
if self._has_any("pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini") or (self.root / "tests").exists():
|
|
38
|
+
commands.append("python3 -m pytest -q")
|
|
39
|
+
|
|
40
|
+
frontend_files = [
|
|
41
|
+
p for p in changed
|
|
42
|
+
if p.endswith((".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".css"))
|
|
43
|
+
]
|
|
44
|
+
if frontend_files and (self.root / "package.json").exists():
|
|
45
|
+
scripts = self._package_scripts()
|
|
46
|
+
reasons.append("Node/TypeScript files changed")
|
|
47
|
+
if "test" in scripts:
|
|
48
|
+
commands.append("npm test")
|
|
49
|
+
if "build" in scripts:
|
|
50
|
+
commands.append("npm run build")
|
|
51
|
+
elif (self.root / "tsconfig.json").exists():
|
|
52
|
+
commands.append("npx tsc --noEmit")
|
|
53
|
+
|
|
54
|
+
if not commands:
|
|
55
|
+
if (self.root / "pyproject.toml").exists() or (self.root / "tests").exists():
|
|
56
|
+
commands.append("python3 -m pytest -q")
|
|
57
|
+
reasons.append("Python project detected")
|
|
58
|
+
elif (self.root / "package.json").exists():
|
|
59
|
+
scripts = self._package_scripts()
|
|
60
|
+
if "test" in scripts:
|
|
61
|
+
commands.append("npm test")
|
|
62
|
+
reasons.append("Node project detected")
|
|
63
|
+
elif "build" in scripts:
|
|
64
|
+
commands.append("npm run build")
|
|
65
|
+
reasons.append("Node project detected")
|
|
66
|
+
|
|
67
|
+
deduped = list(dict.fromkeys(commands))
|
|
68
|
+
reason = "; ".join(dict.fromkeys(reasons)) if reasons else "No focused check inferred"
|
|
69
|
+
return VerificationPlan(deduped, reason)
|
|
70
|
+
|
|
71
|
+
def _has_any(self, *names: str) -> bool:
|
|
72
|
+
return any((self.root / name).exists() for name in names)
|
|
73
|
+
|
|
74
|
+
def _package_scripts(self) -> dict:
|
|
75
|
+
try:
|
|
76
|
+
data = json.loads((self.root / "package.json").read_text(encoding="utf-8"))
|
|
77
|
+
return data.get("scripts", {}) if isinstance(data.get("scripts", {}), dict) else {}
|
|
78
|
+
except Exception:
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
def _git_changed_files(self) -> List[str]:
|
|
82
|
+
try:
|
|
83
|
+
proc = subprocess.run(
|
|
84
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
85
|
+
cwd=self.root,
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
timeout=5,
|
|
89
|
+
check=False,
|
|
90
|
+
)
|
|
91
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
92
|
+
return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
try:
|
|
96
|
+
proc = subprocess.run(
|
|
97
|
+
["git", "status", "--short"],
|
|
98
|
+
cwd=self.root,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=5,
|
|
102
|
+
check=False,
|
|
103
|
+
)
|
|
104
|
+
if proc.returncode != 0:
|
|
105
|
+
return []
|
|
106
|
+
files = []
|
|
107
|
+
for line in proc.stdout.splitlines():
|
|
108
|
+
if not line.strip():
|
|
109
|
+
continue
|
|
110
|
+
files.append(line[3:].strip())
|
|
111
|
+
return files
|
|
112
|
+
except Exception:
|
|
113
|
+
return []
|