patchi 0.7.5__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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project context for context-aware agent gating.
|
|
3
|
+
|
|
4
|
+
Central place to determine whether a security agent is relevant
|
|
5
|
+
to the project being scanned, based on its inferred domain and purpose.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from patchi.core.brain.project_context import agent_is_relevant
|
|
9
|
+
if not agent_is_relevant(inp):
|
|
10
|
+
self.skip(result, "Not relevant for this project type")
|
|
11
|
+
return result
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
_WEB_DOMAINS = {"web-app", "web", "e-commerce", "saas", "social", "cms", "portal"}
|
|
17
|
+
_FINANCE_DOMAINS = {"finance", "banking", "fintech", "payments"}
|
|
18
|
+
_HEALTHCARE_DOMAINS = {"healthcare", "health", "medical", "hipaa"}
|
|
19
|
+
_MOBILE_DOMAINS = {"mobile-app", "android", "ios"}
|
|
20
|
+
_DATA_DOMAINS = {"data-pipeline", "etl", "analytics", "data"}
|
|
21
|
+
|
|
22
|
+
# Domain IDs from the security taxonomy that are web-related
|
|
23
|
+
_WEB_DOMAIN_IDS = {"web-frontend", "auth-session"}
|
|
24
|
+
|
|
25
|
+
# Agents that should only run when the project is a user-facing web app
|
|
26
|
+
_WEB_ONLY_AGENTS = {
|
|
27
|
+
"AuthenticationAuditAgent",
|
|
28
|
+
"AuthZAgent",
|
|
29
|
+
"CORS",
|
|
30
|
+
"HeaderAuditAgent",
|
|
31
|
+
"RateLimitAuditor",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Agents that should skip industry-specific rules unless domain matches
|
|
35
|
+
_INDUSTRY_AGENTS = {
|
|
36
|
+
"PolicyEngine",
|
|
37
|
+
"ComplianceAgent",
|
|
38
|
+
"PrivacyAgent",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Agents that are universal (run on any project)
|
|
42
|
+
_UNIVERSAL_AGENTS = {
|
|
43
|
+
"SecretScanner",
|
|
44
|
+
"EnvScanner",
|
|
45
|
+
"DependencyScanner",
|
|
46
|
+
"CVEMonitorAgent",
|
|
47
|
+
"SupplyChainAgent",
|
|
48
|
+
"PreCheckAgent",
|
|
49
|
+
"IaCScanner",
|
|
50
|
+
"ContainerScanner",
|
|
51
|
+
"SSRFProtection",
|
|
52
|
+
"InjectionAgent",
|
|
53
|
+
"CryptoAgent",
|
|
54
|
+
"NetworkAgent",
|
|
55
|
+
"JWTSecurityAgent",
|
|
56
|
+
"SensitiveDataAgent",
|
|
57
|
+
"MisconfigAgent",
|
|
58
|
+
"TaintAnalyzer",
|
|
59
|
+
"RedTeamAgent",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def agent_is_relevant(
|
|
64
|
+
domain: str,
|
|
65
|
+
purpose: str,
|
|
66
|
+
agent_name: str,
|
|
67
|
+
active_domains: list[str] | None = None,
|
|
68
|
+
) -> tuple[bool, str]:
|
|
69
|
+
"""
|
|
70
|
+
Check whether a security agent is relevant for a project.
|
|
71
|
+
|
|
72
|
+
Uses the active security domain IDs from the Brain's context phase
|
|
73
|
+
when available (preferred), falling back to domain/purpose string matching.
|
|
74
|
+
|
|
75
|
+
Returns (relevant, reason) where reason is empty if relevant, or explains the skip.
|
|
76
|
+
"""
|
|
77
|
+
domain_lower = domain.lower()
|
|
78
|
+
purpose_lower = purpose.lower()
|
|
79
|
+
active = active_domains or []
|
|
80
|
+
|
|
81
|
+
# Universal agents always run
|
|
82
|
+
if agent_name in _UNIVERSAL_AGENTS:
|
|
83
|
+
return True, ""
|
|
84
|
+
|
|
85
|
+
# Web-only agents skip if no web-related domain is activated
|
|
86
|
+
if agent_name in _WEB_ONLY_AGENTS:
|
|
87
|
+
is_web = bool(set(active) & _WEB_DOMAIN_IDS)
|
|
88
|
+
if not is_web:
|
|
89
|
+
is_web = bool(any(d in domain_lower for d in _WEB_DOMAINS) or "web" in purpose_lower)
|
|
90
|
+
if not is_web:
|
|
91
|
+
return False, f"Project domain is '{domain}' — auth checks not applicable"
|
|
92
|
+
|
|
93
|
+
# Industry-specific agents skip unless their domain matches
|
|
94
|
+
if agent_name in _INDUSTRY_AGENTS:
|
|
95
|
+
if not domain_lower and not active:
|
|
96
|
+
return True, ""
|
|
97
|
+
is_finance = any(d in domain_lower for d in _FINANCE_DOMAINS)
|
|
98
|
+
is_health = any(d in domain_lower for d in _HEALTHCARE_DOMAINS)
|
|
99
|
+
if agent_name == "PolicyEngine":
|
|
100
|
+
return True, ""
|
|
101
|
+
if agent_name == "PrivacyAgent":
|
|
102
|
+
if not is_finance and not is_health:
|
|
103
|
+
_has_pii_signals = any(
|
|
104
|
+
w in purpose_lower for w in ("pii", "gdpr", "ccpa", "privacy", "personal")
|
|
105
|
+
)
|
|
106
|
+
if not _has_pii_signals:
|
|
107
|
+
return False, f"Project domain is '{domain}' — privacy rules not applicable"
|
|
108
|
+
|
|
109
|
+
return True, ""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def domain_has_auth(domain: str, routes: list[str]) -> bool:
|
|
113
|
+
"""Quick check if a project likely has authentication based on routes."""
|
|
114
|
+
if domain in _WEB_DOMAINS:
|
|
115
|
+
return True
|
|
116
|
+
auth_keywords = {"login", "signin", "auth", "register", "token"}
|
|
117
|
+
for route in routes:
|
|
118
|
+
for kw in auth_keywords:
|
|
119
|
+
if kw in route.lower():
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart Project Reader — understands what the project is by reading its files.
|
|
3
|
+
|
|
4
|
+
Extracts:
|
|
5
|
+
- Project name and description from README, package.json, pyproject.toml
|
|
6
|
+
- Tech stack from dependencies
|
|
7
|
+
- Entry points and main modules
|
|
8
|
+
- Project structure and architecture patterns
|
|
9
|
+
- Critical paths that must never break
|
|
10
|
+
|
|
11
|
+
This feeds into the ContractBuilder to make the app contract
|
|
12
|
+
actually understand the project, not just pattern-match routes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
_log = logging.getLogger("patchi.brain.project_reader")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ProjectInsight:
|
|
27
|
+
"""What the project reader understands about the project."""
|
|
28
|
+
name: str = ""
|
|
29
|
+
description: str = ""
|
|
30
|
+
tech_stack: list[str] = field(default_factory=list)
|
|
31
|
+
entry_points: list[str] = field(default_factory=list)
|
|
32
|
+
critical_dirs: list[str] = field(default_factory=list)
|
|
33
|
+
project_type: str = "" # "web-app", "cli", "library", "api", "desktop", etc.
|
|
34
|
+
framework: str = ""
|
|
35
|
+
language: str = ""
|
|
36
|
+
dependencies: list[str] = field(default_factory=list)
|
|
37
|
+
dev_dependencies: list[str] = field(default_factory=list)
|
|
38
|
+
scripts: dict[str, str] = field(default_factory=dict)
|
|
39
|
+
readme_summary: str = ""
|
|
40
|
+
architecture_notes: str = ""
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
return {
|
|
44
|
+
"name": self.name,
|
|
45
|
+
"description": self.description,
|
|
46
|
+
"tech_stack": self.tech_stack,
|
|
47
|
+
"entry_points": self.entry_points,
|
|
48
|
+
"critical_dirs": self.critical_dirs,
|
|
49
|
+
"project_type": self.project_type,
|
|
50
|
+
"framework": self.framework,
|
|
51
|
+
"language": self.language,
|
|
52
|
+
"dependencies": self.dependencies[:20],
|
|
53
|
+
"readme_summary": self.readme_summary[:500],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_project_insight(root: Path) -> ProjectInsight:
|
|
58
|
+
"""Read the project and build a comprehensive understanding."""
|
|
59
|
+
insight = ProjectInsight()
|
|
60
|
+
|
|
61
|
+
# Try multiple sources in order of reliability
|
|
62
|
+
_read_readme(root, insight)
|
|
63
|
+
_read_package_json(root, insight)
|
|
64
|
+
_read_pyproject_toml(root, insight)
|
|
65
|
+
_read_setup_cfg(root, insight)
|
|
66
|
+
_read_cargo_toml(root, insight)
|
|
67
|
+
_read_go_mod(root, insight)
|
|
68
|
+
_read_package_json_ts(root, insight)
|
|
69
|
+
|
|
70
|
+
# Infer project type from structure
|
|
71
|
+
_infer_project_type(root, insight)
|
|
72
|
+
|
|
73
|
+
# Find entry points
|
|
74
|
+
_find_entry_points(root, insight)
|
|
75
|
+
|
|
76
|
+
# Find critical directories
|
|
77
|
+
_find_critical_dirs(root, insight)
|
|
78
|
+
|
|
79
|
+
return insight
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read_readme(root: Path, insight: ProjectInsight) -> None:
|
|
83
|
+
"""Extract project info from README files."""
|
|
84
|
+
for name in ["README.md", "README.rst", "README.txt", "README", "readme.md"]:
|
|
85
|
+
readme_path = root / name
|
|
86
|
+
if readme_path.is_file():
|
|
87
|
+
try:
|
|
88
|
+
content = readme_path.read_text(encoding="utf-8", errors="replace")
|
|
89
|
+
lines = content.strip().splitlines()
|
|
90
|
+
|
|
91
|
+
# Extract title (first # heading or first non-empty line)
|
|
92
|
+
for line in lines:
|
|
93
|
+
stripped = line.strip()
|
|
94
|
+
if stripped.startswith("# "):
|
|
95
|
+
insight.name = stripped[2:].strip()
|
|
96
|
+
break
|
|
97
|
+
elif stripped and not insight.name:
|
|
98
|
+
# First non-empty line as fallback
|
|
99
|
+
insight.name = stripped[:80]
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
# Extract description (first paragraph after title)
|
|
103
|
+
found_title = False
|
|
104
|
+
desc_lines = []
|
|
105
|
+
for line in lines:
|
|
106
|
+
stripped = line.strip()
|
|
107
|
+
if stripped.startswith("# "):
|
|
108
|
+
found_title = True
|
|
109
|
+
continue
|
|
110
|
+
if found_title:
|
|
111
|
+
if stripped == "":
|
|
112
|
+
if desc_lines:
|
|
113
|
+
break
|
|
114
|
+
continue
|
|
115
|
+
if stripped.startswith("#"):
|
|
116
|
+
break
|
|
117
|
+
desc_lines.append(stripped)
|
|
118
|
+
|
|
119
|
+
if desc_lines:
|
|
120
|
+
insight.description = " ".join(desc_lines)[:300]
|
|
121
|
+
|
|
122
|
+
# Extract summary for brain context
|
|
123
|
+
insight.readme_summary = "\n".join(lines[:50])[:500]
|
|
124
|
+
break
|
|
125
|
+
except Exception as e:
|
|
126
|
+
_log.debug("Failed to read %s: %s", name, e)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _read_package_json(root: Path, insight: ProjectInsight) -> None:
|
|
130
|
+
"""Extract project info from package.json."""
|
|
131
|
+
pkg_path = root / "package.json"
|
|
132
|
+
if not pkg_path.is_file():
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
data = json.loads(pkg_path.read_text(encoding="utf-8"))
|
|
137
|
+
if not insight.name:
|
|
138
|
+
insight.name = data.get("name", "")
|
|
139
|
+
if not insight.description:
|
|
140
|
+
insight.description = data.get("description", "")
|
|
141
|
+
|
|
142
|
+
# Dependencies
|
|
143
|
+
deps = data.get("dependencies", {})
|
|
144
|
+
dev_deps = data.get("devDependencies", {})
|
|
145
|
+
insight.dependencies = list(deps.keys())
|
|
146
|
+
insight.dev_dependencies = list(dev_deps.keys())
|
|
147
|
+
|
|
148
|
+
# Tech stack from dependencies
|
|
149
|
+
_infer_tech_stack_from_deps(insight, deps, dev_deps)
|
|
150
|
+
|
|
151
|
+
# Scripts
|
|
152
|
+
insight.scripts = data.get("scripts", {})
|
|
153
|
+
|
|
154
|
+
# Entry points
|
|
155
|
+
main = data.get("main", "")
|
|
156
|
+
if main:
|
|
157
|
+
insight.entry_points.append(main)
|
|
158
|
+
|
|
159
|
+
bin_entry = data.get("bin", {})
|
|
160
|
+
if isinstance(bin_entry, str):
|
|
161
|
+
insight.entry_points.append(bin_entry)
|
|
162
|
+
elif isinstance(bin_entry, dict):
|
|
163
|
+
insight.entry_points.extend(bin_entry.values())
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
_log.debug("Failed to read package.json: %s", e)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _read_pyproject_toml(root: Path, insight: ProjectInsight) -> None:
|
|
170
|
+
"""Extract project info from pyproject.toml."""
|
|
171
|
+
toml_path = root / "pyproject.toml"
|
|
172
|
+
if not toml_path.is_file():
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
import tomllib
|
|
177
|
+
except ImportError:
|
|
178
|
+
# Python < 3.11 fallback
|
|
179
|
+
try:
|
|
180
|
+
import tomli as tomllib
|
|
181
|
+
except ImportError:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
data = tomllib.loads(toml_path.read_text(encoding="utf-8"))
|
|
186
|
+
|
|
187
|
+
# Project metadata
|
|
188
|
+
project = data.get("project", {})
|
|
189
|
+
if not insight.name:
|
|
190
|
+
insight.name = project.get("name", "")
|
|
191
|
+
if not insight.description:
|
|
192
|
+
insight.description = project.get("description", "")
|
|
193
|
+
|
|
194
|
+
insight.language = "python"
|
|
195
|
+
|
|
196
|
+
# Dependencies
|
|
197
|
+
deps = project.get("dependencies", [])
|
|
198
|
+
insight.dependencies = [_dep_name(d) for d in deps]
|
|
199
|
+
|
|
200
|
+
optional = project.get("optional-dependencies", {})
|
|
201
|
+
for group_deps in optional.values():
|
|
202
|
+
insight.dev_dependencies.extend([_dep_name(d) for d in group_deps])
|
|
203
|
+
|
|
204
|
+
# Scripts / entry points
|
|
205
|
+
scripts = data.get("project", {}).get("scripts", {})
|
|
206
|
+
if scripts:
|
|
207
|
+
insight.scripts.update(scripts)
|
|
208
|
+
insight.entry_points.extend(scripts.values())
|
|
209
|
+
|
|
210
|
+
# Build system hints
|
|
211
|
+
build = data.get("build-system", {})
|
|
212
|
+
build_backend = build.get("build-backend", "")
|
|
213
|
+
if "setuptools" in build_backend:
|
|
214
|
+
insight.tech_stack.append("setuptools")
|
|
215
|
+
elif "poetry" in build_backend:
|
|
216
|
+
insight.tech_stack.append("poetry")
|
|
217
|
+
elif "hatchling" in build_backend:
|
|
218
|
+
insight.tech_stack.append("hatchling")
|
|
219
|
+
|
|
220
|
+
# Tool configs
|
|
221
|
+
if "ruff" in data:
|
|
222
|
+
insight.dev_dependencies.append("ruff")
|
|
223
|
+
if "pytest" in data:
|
|
224
|
+
insight.dev_dependencies.append("pytest")
|
|
225
|
+
if "mypy" in data:
|
|
226
|
+
insight.dev_dependencies.append("mypy")
|
|
227
|
+
|
|
228
|
+
# Infer tech stack
|
|
229
|
+
_infer_tech_stack_from_deps(
|
|
230
|
+
insight,
|
|
231
|
+
dict.fromkeys(insight.dependencies, ""),
|
|
232
|
+
dict.fromkeys(insight.dev_dependencies, ""),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
_log.debug("Failed to read pyproject.toml: %s", e)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _read_setup_cfg(root: Path, insight: ProjectInsight) -> None:
|
|
240
|
+
"""Extract from setup.cfg."""
|
|
241
|
+
setup_path = root / "setup.cfg"
|
|
242
|
+
if not setup_path.is_file():
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
import configparser
|
|
247
|
+
cfg = configparser.ConfigParser()
|
|
248
|
+
cfg.read(str(setup_path))
|
|
249
|
+
|
|
250
|
+
if not insight.name:
|
|
251
|
+
insight.name = cfg.get("metadata", "name", fallback="")
|
|
252
|
+
if not insight.description:
|
|
253
|
+
insight.description = cfg.get("metadata", "description", fallback="")
|
|
254
|
+
insight.language = "python"
|
|
255
|
+
except Exception as _exc:
|
|
256
|
+
_log.debug('_read_setup_cfg skipped: %s', _exc)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _read_cargo_toml(root: Path, insight: ProjectInsight) -> None:
|
|
260
|
+
"""Extract from Cargo.toml (Rust projects)."""
|
|
261
|
+
cargo_path = root / "Cargo.toml"
|
|
262
|
+
if not cargo_path.is_file():
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
import tomllib
|
|
267
|
+
except ImportError:
|
|
268
|
+
try:
|
|
269
|
+
import tomli as tomllib
|
|
270
|
+
except ImportError:
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
data = tomllib.loads(cargo_path.read_text(encoding="utf-8"))
|
|
275
|
+
pkg = data.get("package", {})
|
|
276
|
+
insight.name = pkg.get("name", "")
|
|
277
|
+
insight.description = pkg.get("description", "")
|
|
278
|
+
insight.language = "rust"
|
|
279
|
+
insight.tech_stack.append("cargo")
|
|
280
|
+
|
|
281
|
+
deps = data.get("dependencies", {})
|
|
282
|
+
insight.dependencies = list(deps.keys())
|
|
283
|
+
except Exception as _exc:
|
|
284
|
+
_log.debug('_read_cargo_toml skipped: %s', _exc)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _read_go_mod(root: Path, insight: ProjectInsight) -> None:
|
|
288
|
+
"""Extract from go.mod (Go projects)."""
|
|
289
|
+
go_mod = root / "go.mod"
|
|
290
|
+
if not go_mod.is_file():
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
content = go_mod.read_text(encoding="utf-8")
|
|
295
|
+
for line in content.splitlines():
|
|
296
|
+
if line.startswith("module "):
|
|
297
|
+
insight.name = line.split()[-1].split("/")[-1]
|
|
298
|
+
insight.language = "go"
|
|
299
|
+
break
|
|
300
|
+
except Exception as _exc:
|
|
301
|
+
_log.debug('_read_go_mod skipped: %s', _exc)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _read_package_json_ts(root: Path, insight: ProjectInsight) -> None:
|
|
305
|
+
"""Read tsconfig.json for TypeScript projects."""
|
|
306
|
+
tsconfig = root / "tsconfig.json"
|
|
307
|
+
if not tsconfig.is_file():
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
data = json.loads(tsconfig.read_text(encoding="utf-8"))
|
|
312
|
+
if "compilerOptions" in data:
|
|
313
|
+
insight.tech_stack.append("typescript")
|
|
314
|
+
if insight.language == "":
|
|
315
|
+
insight.language = "typescript"
|
|
316
|
+
except Exception as _exc:
|
|
317
|
+
_log.debug('_read_package_json_ts skipped: %s', _exc)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _infer_tech_stack_from_deps(
|
|
321
|
+
insight: ProjectInsight,
|
|
322
|
+
deps: dict,
|
|
323
|
+
dev_deps: dict,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Infer tech stack from dependency names."""
|
|
326
|
+
all_deps = set(list(deps.keys()) + list(dev_deps.keys()))
|
|
327
|
+
|
|
328
|
+
framework_hints = {
|
|
329
|
+
"fastapi": ("FastAPI", "web-app", "python"),
|
|
330
|
+
"flask": ("Flask", "web-app", "python"),
|
|
331
|
+
"django": ("Django", "web-app", "python"),
|
|
332
|
+
"starlette": ("Starlette", "web-app", "python"),
|
|
333
|
+
"uvicorn": ("uvicorn", "web-app", "python"),
|
|
334
|
+
"react": ("React", "web-app", "javascript"),
|
|
335
|
+
"vue": ("Vue", "web-app", "javascript"),
|
|
336
|
+
"angular": ("Angular", "web-app", "javascript"),
|
|
337
|
+
"next": ("Next.js", "web-app", "javascript"),
|
|
338
|
+
"svelte": ("Svelte", "web-app", "javascript"),
|
|
339
|
+
"express": ("Express", "api", "javascript"),
|
|
340
|
+
"koa": ("Koa", "api", "javascript"),
|
|
341
|
+
"hono": ("Hono", "api", "javascript"),
|
|
342
|
+
"gin": ("Gin", "api", "go"),
|
|
343
|
+
"fiber": ("Fiber", "api", "go"),
|
|
344
|
+
"actix": ("Actix", "api", "rust"),
|
|
345
|
+
"axum": ("Axum", "api", "rust"),
|
|
346
|
+
"tauri": ("Tauri", "desktop", "rust"),
|
|
347
|
+
"electron": ("Electron", "desktop", "javascript"),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for dep_name, (fw, ptype, lang) in framework_hints.items():
|
|
351
|
+
if dep_name in all_deps:
|
|
352
|
+
if fw not in insight.tech_stack:
|
|
353
|
+
insight.tech_stack.append(fw)
|
|
354
|
+
if not insight.project_type:
|
|
355
|
+
insight.project_type = ptype
|
|
356
|
+
if not insight.language:
|
|
357
|
+
insight.language = lang
|
|
358
|
+
insight.framework = fw
|
|
359
|
+
|
|
360
|
+
# Dev tool hints
|
|
361
|
+
dev_tools = {
|
|
362
|
+
"pytest": "pytest",
|
|
363
|
+
"jest": "jest",
|
|
364
|
+
"vitest": "vitest",
|
|
365
|
+
"mocha": "mocha",
|
|
366
|
+
"eslint": "eslint",
|
|
367
|
+
"prettier": "prettier",
|
|
368
|
+
"ruff": "ruff",
|
|
369
|
+
"mypy": "mypy",
|
|
370
|
+
"typescript": "typescript",
|
|
371
|
+
"playwright": "playwright",
|
|
372
|
+
"cypress": "cypress",
|
|
373
|
+
}
|
|
374
|
+
for tool_name, tool_label in dev_tools.items():
|
|
375
|
+
if tool_name in all_deps and tool_label not in insight.tech_stack:
|
|
376
|
+
insight.tech_stack.append(tool_label)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _infer_project_type(root: Path, insight: ProjectInsight) -> None:
|
|
380
|
+
"""Infer project type from structure if not already known."""
|
|
381
|
+
if insight.project_type:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
indicators = {
|
|
385
|
+
"web-app": [
|
|
386
|
+
"templates/", "static/", "public/", "pages/", "components/",
|
|
387
|
+
"app.py", "main.py", "server.py", "index.html",
|
|
388
|
+
],
|
|
389
|
+
"api": [
|
|
390
|
+
"routes/", "handlers/", "controllers/", "endpoints/",
|
|
391
|
+
"api/", "openapi", "swagger",
|
|
392
|
+
],
|
|
393
|
+
"cli": [
|
|
394
|
+
"cli/", "commands/", "cli.py", "main.py", "__main__.py",
|
|
395
|
+
],
|
|
396
|
+
"library": [
|
|
397
|
+
"src/", "lib/", "__init__.py",
|
|
398
|
+
],
|
|
399
|
+
"desktop": [
|
|
400
|
+
"electron/", "tauri/", "src-tauri/", "main.js",
|
|
401
|
+
],
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
scores: dict[str, int] = {}
|
|
405
|
+
for ptype, indicators_list in indicators.items():
|
|
406
|
+
score = 0
|
|
407
|
+
for indicator in indicators_list:
|
|
408
|
+
if (root / indicator).exists():
|
|
409
|
+
score += 1
|
|
410
|
+
if score:
|
|
411
|
+
scores[ptype] = score
|
|
412
|
+
|
|
413
|
+
if scores:
|
|
414
|
+
insight.project_type = max(scores, key=scores.get)
|
|
415
|
+
else:
|
|
416
|
+
insight.project_type = "unknown"
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _find_entry_points(root: Path, insight: ProjectInsight) -> None:
|
|
420
|
+
"""Find main entry points of the project."""
|
|
421
|
+
# Check pyproject.toml scripts
|
|
422
|
+
if insight.scripts:
|
|
423
|
+
for name, cmd in insight.scripts.items():
|
|
424
|
+
if "python" in cmd or "patchi" in cmd:
|
|
425
|
+
insight.entry_points.append(f"pyproject:{name}")
|
|
426
|
+
|
|
427
|
+
# Check common entry points
|
|
428
|
+
candidates = [
|
|
429
|
+
"main.py", "app.py", "server.py", "index.py",
|
|
430
|
+
"main.js", "index.js", "app.js", "server.js",
|
|
431
|
+
"main.ts", "index.ts", "app.ts",
|
|
432
|
+
"src/main.rs", "src/lib.rs",
|
|
433
|
+
"main.go",
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
for candidate in candidates:
|
|
437
|
+
path = root / candidate
|
|
438
|
+
if path.exists() and candidate not in insight.entry_points:
|
|
439
|
+
insight.entry_points.append(candidate)
|
|
440
|
+
|
|
441
|
+
# Check for __main__.py (Python package entry point)
|
|
442
|
+
for pkg_dir in ["patchi", "src"]:
|
|
443
|
+
main_file = root / pkg_dir / "__main__.py"
|
|
444
|
+
if main_file.exists():
|
|
445
|
+
insight.entry_points.append(f"{pkg_dir}/__main__.py")
|
|
446
|
+
|
|
447
|
+
# Check for CLI entry points in setup.py/setup.cfg
|
|
448
|
+
for setup_file in ["setup.py", "setup.cfg"]:
|
|
449
|
+
setup_path = root / setup_file
|
|
450
|
+
if setup_path.is_file():
|
|
451
|
+
try:
|
|
452
|
+
content = setup_path.read_text(encoding="utf-8", errors="replace")
|
|
453
|
+
if "console_scripts" in content or "entry_points" in content:
|
|
454
|
+
insight.entry_points.append(setup_file)
|
|
455
|
+
except Exception as _exc:
|
|
456
|
+
_log.debug('_find_entry_points skipped: %s', _exc)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _find_critical_dirs(root: Path, insight: ProjectInsight) -> None:
|
|
460
|
+
"""Find directories that are critical to the project."""
|
|
461
|
+
# Skip common non-critical directories
|
|
462
|
+
skip_dirs = {
|
|
463
|
+
".git", ".venv", "venv", "env", "__pycache__", "node_modules",
|
|
464
|
+
".mypy_cache", ".pytest_cache", ".ruff_cache", "dist", "build",
|
|
465
|
+
".eggs", "*.egg-info", ".tox", ".nox", "htmlcov",
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
# Patterns that indicate critical directories
|
|
469
|
+
critical_patterns = {
|
|
470
|
+
"auth": ["auth", "authentication", "authorization", "login", "session"],
|
|
471
|
+
"api": ["api", "routes", "handlers", "controllers", "endpoints"],
|
|
472
|
+
"models": ["models", "schemas", "entities", "types"],
|
|
473
|
+
"services": ["services", "business", "domain", "core"],
|
|
474
|
+
"database": ["db", "database", "migrations", "models"],
|
|
475
|
+
"config": ["config", "settings", "env"],
|
|
476
|
+
"cli": ["cli", "commands"],
|
|
477
|
+
"web": ["web", "templates", "static", "views"],
|
|
478
|
+
"core": ["core", "lib", "src"],
|
|
479
|
+
"tests": ["tests", "test", "spec"],
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for dirpath in [d for d in root.iterdir() if d.is_dir()]:
|
|
483
|
+
if dirpath.name.startswith(".") or dirpath.name in skip_dirs:
|
|
484
|
+
continue
|
|
485
|
+
# Skip directories with only __pycache__ or hidden files
|
|
486
|
+
try:
|
|
487
|
+
contents = list(dirpath.iterdir())
|
|
488
|
+
real_contents = [c for c in contents if not c.name.startswith(".") and c.name != "__pycache__"]
|
|
489
|
+
if not real_contents:
|
|
490
|
+
continue
|
|
491
|
+
except PermissionError:
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
dir_lower = dirpath.name.lower()
|
|
495
|
+
matched = False
|
|
496
|
+
for _category, patterns in critical_patterns.items():
|
|
497
|
+
if any(p in dir_lower for p in patterns):
|
|
498
|
+
if dirpath.name not in insight.critical_dirs:
|
|
499
|
+
insight.critical_dirs.append(dirpath.name)
|
|
500
|
+
matched = True
|
|
501
|
+
break
|
|
502
|
+
|
|
503
|
+
# If no pattern matched, check if it's a source code directory
|
|
504
|
+
if not matched:
|
|
505
|
+
# Check if it has Python/JS/TS files
|
|
506
|
+
has_source = False
|
|
507
|
+
try:
|
|
508
|
+
for f in dirpath.iterdir():
|
|
509
|
+
if f.suffix in (".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs"):
|
|
510
|
+
has_source = True
|
|
511
|
+
break
|
|
512
|
+
except PermissionError:
|
|
513
|
+
pass
|
|
514
|
+
if has_source and dirpath.name not in insight.critical_dirs:
|
|
515
|
+
insight.critical_dirs.append(dirpath.name)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _dep_name(dep_string: str) -> str:
|
|
519
|
+
"""Extract package name from a dependency string like 'requests>=2.0'."""
|
|
520
|
+
name = dep_string.split(">")[0].split("<")[0].split("=")[0].split("!")[0].split("[")[0].strip()
|
|
521
|
+
return name.lower().replace("-", "_")
|