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,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain freshness tracker.
|
|
3
|
+
|
|
4
|
+
Determines whether the Brain's knowledge is still current by comparing
|
|
5
|
+
file modification times against the last scan timestamp.
|
|
6
|
+
|
|
7
|
+
Also provides the file watcher used by `p watch`.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
FRESHNESS_FILE = ".patchi/brain_freshness.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Freshness record ───────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_log = logging.getLogger("patchi.brain.freshness")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def save_freshness_snapshot(root: Path, file_infos_paths: list[str]) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Record the current mtime of every scanned file.
|
|
30
|
+
Called at the end of a successful brain scan.
|
|
31
|
+
"""
|
|
32
|
+
snapshot: dict[str, float] = {}
|
|
33
|
+
for rel_path in file_infos_paths:
|
|
34
|
+
abs_path = root / rel_path
|
|
35
|
+
try:
|
|
36
|
+
snapshot[rel_path] = abs_path.stat().st_mtime
|
|
37
|
+
except OSError:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
freshness_path = root / FRESHNESS_FILE
|
|
41
|
+
freshness_path.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
with freshness_path.open("w", encoding="utf-8") as f:
|
|
43
|
+
json.dump(
|
|
44
|
+
{
|
|
45
|
+
"snapshot": snapshot,
|
|
46
|
+
"recorded_at": datetime.now(UTC).isoformat(),
|
|
47
|
+
},
|
|
48
|
+
f,
|
|
49
|
+
indent=2,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_freshness(root: Path) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
Compare current file mtimes to the saved snapshot.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
{
|
|
59
|
+
"is_stale": bool,
|
|
60
|
+
"changed_files": [list of changed rel paths],
|
|
61
|
+
"new_files": [list of new files not in snapshot],
|
|
62
|
+
"deleted_files": [list of files that no longer exist],
|
|
63
|
+
"last_recorded": ISO timestamp or None,
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
freshness_path = root / FRESHNESS_FILE
|
|
67
|
+
if not freshness_path.exists():
|
|
68
|
+
return {
|
|
69
|
+
"is_stale": True,
|
|
70
|
+
"changed_files": [],
|
|
71
|
+
"new_files": [],
|
|
72
|
+
"deleted_files": [],
|
|
73
|
+
"last_recorded": None,
|
|
74
|
+
"reason": "No scan recorded yet.",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
with freshness_path.open("r", encoding="utf-8") as f:
|
|
79
|
+
data = json.load(f)
|
|
80
|
+
except (json.JSONDecodeError, OSError):
|
|
81
|
+
return {
|
|
82
|
+
"is_stale": True,
|
|
83
|
+
"changed_files": [],
|
|
84
|
+
"new_files": [],
|
|
85
|
+
"deleted_files": [],
|
|
86
|
+
"last_recorded": None,
|
|
87
|
+
"reason": "Freshness file is corrupted.",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
snapshot: dict[str, float] = data.get("snapshot", {})
|
|
91
|
+
last_recorded: str = data.get("recorded_at", "")
|
|
92
|
+
|
|
93
|
+
changed: list[str] = []
|
|
94
|
+
deleted: list[str] = []
|
|
95
|
+
|
|
96
|
+
for rel_path, old_mtime in snapshot.items():
|
|
97
|
+
abs_path = root / rel_path
|
|
98
|
+
try:
|
|
99
|
+
current_mtime = abs_path.stat().st_mtime
|
|
100
|
+
if current_mtime != old_mtime:
|
|
101
|
+
changed.append(rel_path)
|
|
102
|
+
except OSError:
|
|
103
|
+
deleted.append(rel_path)
|
|
104
|
+
|
|
105
|
+
# New files: scan for any source files not in snapshot
|
|
106
|
+
# (lightweight — just check common source roots)
|
|
107
|
+
new_files = _find_new_files(root, snapshot)
|
|
108
|
+
|
|
109
|
+
is_stale = bool(changed or deleted or new_files)
|
|
110
|
+
reason = ""
|
|
111
|
+
if is_stale:
|
|
112
|
+
parts: list[str] = []
|
|
113
|
+
if changed:
|
|
114
|
+
parts.append(f"{len(changed)} file(s) modified")
|
|
115
|
+
if deleted:
|
|
116
|
+
parts.append(f"{len(deleted)} file(s) deleted")
|
|
117
|
+
if new_files:
|
|
118
|
+
parts.append(f"{len(new_files)} new file(s) found")
|
|
119
|
+
reason = "; ".join(parts) + " since last scan."
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"is_stale": is_stale,
|
|
123
|
+
"changed_files": changed,
|
|
124
|
+
"new_files": new_files,
|
|
125
|
+
"deleted_files": deleted,
|
|
126
|
+
"last_recorded": last_recorded,
|
|
127
|
+
"reason": reason,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _find_new_files(root: Path, snapshot: dict[str, float]) -> list[str]:
|
|
132
|
+
"""
|
|
133
|
+
Quick scan for source files that exist now but weren't in the snapshot.
|
|
134
|
+
Checks only source directories, not node_modules etc.
|
|
135
|
+
"""
|
|
136
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS, Lang, detect_language
|
|
137
|
+
|
|
138
|
+
new_files: list[str] = []
|
|
139
|
+
count = 0
|
|
140
|
+
|
|
141
|
+
for dirpath, dirnames, filenames in root_walk(root):
|
|
142
|
+
# Prune ignored dirs
|
|
143
|
+
dirnames[:] = [d for d in dirnames if d not in DEFAULT_IGNORE_DIRS]
|
|
144
|
+
for fname in filenames:
|
|
145
|
+
fpath = Path(dirpath) / fname
|
|
146
|
+
try:
|
|
147
|
+
rel = fpath.relative_to(root).as_posix()
|
|
148
|
+
except ValueError:
|
|
149
|
+
continue
|
|
150
|
+
if rel not in snapshot:
|
|
151
|
+
lang = detect_language(fpath)
|
|
152
|
+
if lang != Lang.UNKNOWN:
|
|
153
|
+
new_files.append(rel)
|
|
154
|
+
count += 1
|
|
155
|
+
if count > 5000: # bail early on huge projects
|
|
156
|
+
return new_files
|
|
157
|
+
|
|
158
|
+
return new_files
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def root_walk(root: Path):
|
|
162
|
+
"""os.walk wrapper that stays inside root."""
|
|
163
|
+
import os
|
|
164
|
+
|
|
165
|
+
yield from os.walk(root)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ── File watcher (for p watch) ─────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class BrainWatcher:
|
|
172
|
+
"""
|
|
173
|
+
Watches the project directory for file changes using watchfiles (Rust-backed).
|
|
174
|
+
|
|
175
|
+
watchfiles is significantly faster than watchdog — it uses native OS file
|
|
176
|
+
events (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows)
|
|
177
|
+
via a Rust extension. Debouncing is built-in.
|
|
178
|
+
|
|
179
|
+
On change: marks brain stale and calls on_change callback with changed paths.
|
|
180
|
+
|
|
181
|
+
Usage:
|
|
182
|
+
watcher = BrainWatcher(root, on_change=lambda paths: ...)
|
|
183
|
+
watcher.start() # blocks until stop() is called
|
|
184
|
+
watcher.stop()
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
root: Path,
|
|
190
|
+
on_change: Callable[[list[str]], None],
|
|
191
|
+
area: str | None = None,
|
|
192
|
+
debounce_ms: int = 500,
|
|
193
|
+
):
|
|
194
|
+
self.root = root
|
|
195
|
+
self.on_change = on_change
|
|
196
|
+
self.area = area
|
|
197
|
+
self.debounce_ms = debounce_ms
|
|
198
|
+
self._stop_event: threading.Event | None = None
|
|
199
|
+
self._thread: threading.Thread | None = None
|
|
200
|
+
|
|
201
|
+
def start(self) -> None:
|
|
202
|
+
"""Start watching in a background thread. Returns immediately."""
|
|
203
|
+
import threading
|
|
204
|
+
|
|
205
|
+
self._stop_event = threading.Event()
|
|
206
|
+
self._thread = threading.Thread(
|
|
207
|
+
target=self._watch_loop,
|
|
208
|
+
daemon=True,
|
|
209
|
+
name="BrainWatcher",
|
|
210
|
+
)
|
|
211
|
+
self._thread.start()
|
|
212
|
+
|
|
213
|
+
def _watch_loop(self) -> None:
|
|
214
|
+
"""Background thread: runs watchfiles.watch() and dispatches changes."""
|
|
215
|
+
try:
|
|
216
|
+
from watchfiles import Change # noqa: F401 — used to check availability
|
|
217
|
+
from watchfiles import watch as wf_watch
|
|
218
|
+
except ImportError as e:
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
"watchfiles is required for p watch. Install it with: pip install watchfiles"
|
|
221
|
+
) from e
|
|
222
|
+
|
|
223
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS, Lang, detect_language
|
|
224
|
+
|
|
225
|
+
watch_path = str(self.root / self.area) if self.area else str(self.root)
|
|
226
|
+
|
|
227
|
+
# watchfiles accepts stop_event directly — clean shutdown
|
|
228
|
+
for changes in wf_watch(
|
|
229
|
+
watch_path,
|
|
230
|
+
debounce=self.debounce_ms,
|
|
231
|
+
stop_event=self._stop_event,
|
|
232
|
+
ignore_permission_denied=True,
|
|
233
|
+
):
|
|
234
|
+
if self._stop_event and self._stop_event.is_set():
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
changed_paths: list[str] = []
|
|
238
|
+
for _change_type, raw_path in changes:
|
|
239
|
+
path = Path(raw_path)
|
|
240
|
+
|
|
241
|
+
# Skip patchi internals and build artifacts
|
|
242
|
+
if any(part in DEFAULT_IGNORE_DIRS for part in path.parts):
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
lang = detect_language(path)
|
|
246
|
+
if lang == Lang.UNKNOWN:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
rel = path.relative_to(self.root).as_posix()
|
|
251
|
+
changed_paths.append(rel)
|
|
252
|
+
except ValueError:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
if changed_paths:
|
|
256
|
+
# Mark brain stale for these files
|
|
257
|
+
try:
|
|
258
|
+
from patchi.core import memory as mem
|
|
259
|
+
|
|
260
|
+
mem.mark_brain_stale(changed_paths, self.root)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
_log.warning("BrainWatcher._watch_loop failed: %s", e)
|
|
263
|
+
self.on_change(changed_paths)
|
|
264
|
+
|
|
265
|
+
def stop(self) -> None:
|
|
266
|
+
"""Signal the watcher to stop and wait for the thread to exit."""
|
|
267
|
+
if self._stop_event:
|
|
268
|
+
self._stop_event.set()
|
|
269
|
+
if self._thread and self._thread.is_alive():
|
|
270
|
+
self._thread.join(timeout=3.0)
|
|
271
|
+
|
|
272
|
+
def is_alive(self) -> bool:
|
|
273
|
+
return self._thread is not None and self._thread.is_alive()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git-aware intelligence for Patchi.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Incremental scan: only scan files changed since last scan (via git diff)
|
|
6
|
+
- Blame integration: trace which commit introduced a finding
|
|
7
|
+
- Changelog generation: summarize what changed between scans
|
|
8
|
+
|
|
9
|
+
Uses subprocess to call git — no Python git library required.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from patchi.core.agents.base import Finding
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger("patchi.brain.git_aware")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _git(args: list[str], root: Path) -> str | None:
|
|
24
|
+
"""Run a git command and return stdout, or None on failure."""
|
|
25
|
+
try:
|
|
26
|
+
result = subprocess.run(
|
|
27
|
+
["git"] + args,
|
|
28
|
+
cwd=str(root),
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=10,
|
|
32
|
+
)
|
|
33
|
+
if result.returncode == 0:
|
|
34
|
+
return result.stdout.strip()
|
|
35
|
+
return None
|
|
36
|
+
except Exception as e:
|
|
37
|
+
_log.debug("_git failed: %s", e)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_git_repo(root: Path) -> bool:
|
|
42
|
+
"""Check if the project is a git repository."""
|
|
43
|
+
return _git(["rev-parse", "--is-inside-work-tree"], root) == "true"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_changed_files(root: Path, since: str = "HEAD") -> list[str]:
|
|
47
|
+
"""
|
|
48
|
+
Get list of files changed since a ref (commit, tag, branch).
|
|
49
|
+
Default: changes since last commit.
|
|
50
|
+
Returns relative paths.
|
|
51
|
+
"""
|
|
52
|
+
output = _git(["diff", "--name-only", since], root)
|
|
53
|
+
if output is None:
|
|
54
|
+
# Try diffing against empty tree (all files)
|
|
55
|
+
output = _git(["diff", "--name-only", "--diff-filter=ACMR", "HEAD~1"], root)
|
|
56
|
+
if output is None:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
# Also include untracked files
|
|
60
|
+
untracked = _git(["ls-files", "--others", "--exclude-standard"], root)
|
|
61
|
+
|
|
62
|
+
files = [f for f in output.splitlines() if f.strip()]
|
|
63
|
+
if untracked:
|
|
64
|
+
files.extend(f for f in untracked.splitlines() if f.strip())
|
|
65
|
+
|
|
66
|
+
return list(set(files))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_changed_since_scan(root: Path, last_scan_time: float | None = None) -> list[str]:
|
|
70
|
+
"""
|
|
71
|
+
Get files changed since the last scan timestamp.
|
|
72
|
+
Falls back to git log if timestamps aren't available.
|
|
73
|
+
"""
|
|
74
|
+
if last_scan_time is None:
|
|
75
|
+
# No previous scan — return all files
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
# Use git log to find commits since last scan
|
|
79
|
+
import time
|
|
80
|
+
|
|
81
|
+
since_date = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(last_scan_time))
|
|
82
|
+
|
|
83
|
+
output = _git(["log", "--since=" + since_date, "--name-only", "--pretty=format:"], root)
|
|
84
|
+
if output is None:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
files = [f for f in output.splitlines() if f.strip()]
|
|
88
|
+
return list(set(files))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def blame_line(file_path: str, line: int, root: Path) -> dict | None:
|
|
92
|
+
"""
|
|
93
|
+
Git blame a specific line. Returns:
|
|
94
|
+
{"commit": "abc123", "author": "Name", "date": "2024-01-01", "summary": "msg"}
|
|
95
|
+
"""
|
|
96
|
+
output = _git(["blame", "-L", f"{line},{line}", "--porcelain", file_path], root)
|
|
97
|
+
if not output:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
result: dict[str, str] = {}
|
|
101
|
+
for line_text in output.splitlines():
|
|
102
|
+
# Porcelain format: first line is "<hash> <orig-line> <final-line> <count>"
|
|
103
|
+
# No "commit " prefix — the hash IS the first token on the first line.
|
|
104
|
+
if not result.get("commit") and line_text and not line_text.startswith("\t"):
|
|
105
|
+
parts = line_text.split()
|
|
106
|
+
if parts and len(parts[0]) == 40 and all(c in "0123456789abcdef" for c in parts[0]):
|
|
107
|
+
result["commit"] = parts[0]
|
|
108
|
+
elif line_text.startswith("author "):
|
|
109
|
+
result["author"] = line_text[7:]
|
|
110
|
+
elif line_text.startswith("author-time "):
|
|
111
|
+
import time
|
|
112
|
+
|
|
113
|
+
ts = int(line_text[12:])
|
|
114
|
+
result["date"] = time.strftime("%Y-%m-%d", time.localtime(ts))
|
|
115
|
+
elif line_text.startswith("summary "):
|
|
116
|
+
result["summary"] = line_text[8:]
|
|
117
|
+
|
|
118
|
+
return result if result.get("commit") else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def annotate_findings_with_blame(findings: list, root: Path, max_workers: int = 4) -> int:
|
|
122
|
+
"""
|
|
123
|
+
Enrich findings with git blame information.
|
|
124
|
+
|
|
125
|
+
For each finding with a file + line, runs `git blame` and stores
|
|
126
|
+
result in finding.extra["blame"] as {"commit", "author", "date", "summary"}.
|
|
127
|
+
|
|
128
|
+
Uses parallel processing for speed. Skips non-git repos silently.
|
|
129
|
+
Returns count of annotated findings.
|
|
130
|
+
"""
|
|
131
|
+
if not is_git_repo(root):
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
annotated = 0
|
|
135
|
+
import concurrent.futures
|
|
136
|
+
|
|
137
|
+
def _blame_one(f: Finding) -> Finding:
|
|
138
|
+
if not f.file or f.line <= 0:
|
|
139
|
+
return f
|
|
140
|
+
try:
|
|
141
|
+
info = blame_line(f.file, f.line, root)
|
|
142
|
+
if info:
|
|
143
|
+
f.extra["blame"] = info
|
|
144
|
+
except Exception as e:
|
|
145
|
+
_log.debug("_blame_one failed: %s", e)
|
|
146
|
+
return f
|
|
147
|
+
|
|
148
|
+
batch = [f for f in findings if isinstance(f, Finding) and f.file and f.line > 0]
|
|
149
|
+
if not batch:
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
# Cap per-agent blame calls to avoid excessive subprocess overhead on Windows
|
|
153
|
+
if len(batch) > 200:
|
|
154
|
+
batch = batch[:200]
|
|
155
|
+
|
|
156
|
+
if len(batch) == 1 or max_workers <= 1:
|
|
157
|
+
for f in batch:
|
|
158
|
+
_blame_one(f)
|
|
159
|
+
if f.extra.get("blame"):
|
|
160
|
+
annotated += 1
|
|
161
|
+
else:
|
|
162
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
163
|
+
for f in pool.map(_blame_one, batch):
|
|
164
|
+
if f.extra.get("blame"):
|
|
165
|
+
annotated += 1
|
|
166
|
+
|
|
167
|
+
return annotated
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def changelog(root: Path, since: str = "HEAD~10") -> list[dict]:
|
|
171
|
+
"""
|
|
172
|
+
Generate a changelog from git log.
|
|
173
|
+
Returns list of {"hash", "author", "date", "summary", "files"}.
|
|
174
|
+
"""
|
|
175
|
+
output = _git(
|
|
176
|
+
["log", since, "--pretty=format:%H|%an|%ad|%s", "--date=short"],
|
|
177
|
+
root,
|
|
178
|
+
)
|
|
179
|
+
if not output:
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
entries = []
|
|
183
|
+
for line in output.splitlines():
|
|
184
|
+
if not line.strip():
|
|
185
|
+
continue
|
|
186
|
+
parts = line.split("|", 3)
|
|
187
|
+
if len(parts) < 4:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
commit_hash, author, date, summary = parts
|
|
191
|
+
|
|
192
|
+
# Get files changed in this commit
|
|
193
|
+
files_output = _git(["diff-tree", "--no-commit-id", "-r", "--name-only", commit_hash], root)
|
|
194
|
+
files = [f for f in (files_output or "").splitlines() if f.strip()]
|
|
195
|
+
|
|
196
|
+
entries.append(
|
|
197
|
+
{
|
|
198
|
+
"hash": commit_hash[:8],
|
|
199
|
+
"author": author,
|
|
200
|
+
"date": date,
|
|
201
|
+
"summary": summary,
|
|
202
|
+
"files": files[:20], # Cap at 20 files
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return entries
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Risk Heatmap §9.1.3 — file map colored by bug-prone score.
|
|
3
|
+
|
|
4
|
+
Score per file: findings * 10 + churn * 2 + (size_kb / 10) + complexity_hint.
|
|
5
|
+
Color intensity mapped to folder tree for web D3 treemap.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from patchi.core.brain.heatmap import build_heatmap
|
|
9
|
+
data = build_heatmap(root) # {name, children, value, severity}
|
|
10
|
+
# CLI: p heatmap | p heatmap --json
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from patchi.core import memory as mem
|
|
20
|
+
|
|
21
|
+
_log = logging.getLogger("patchi.brain.heatmap")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _git_churn(root: Path, rel: str) -> int:
|
|
25
|
+
try:
|
|
26
|
+
import subprocess
|
|
27
|
+
|
|
28
|
+
out = subprocess.run(
|
|
29
|
+
["git", "log", "--oneline", "--", rel],
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
timeout=5,
|
|
33
|
+
cwd=str(root),
|
|
34
|
+
)
|
|
35
|
+
if out.returncode == 0:
|
|
36
|
+
return len([line for line in out.stdout.splitlines() if line.strip()])
|
|
37
|
+
except Exception as exc: # noqa: BLE001
|
|
38
|
+
_log.debug("churn %s failed: %s", rel, exc)
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_heatmap(root: Path) -> dict:
|
|
43
|
+
scans = mem.get_scan_results(root) or {}
|
|
44
|
+
# findings per file
|
|
45
|
+
per_file: dict[str, int] = defaultdict(int)
|
|
46
|
+
sev_max: dict[str, str] = {}
|
|
47
|
+
order = {"critical": 4, "high": 3, "medium": 2, "low": 1, "info": 0}
|
|
48
|
+
for data in scans.values():
|
|
49
|
+
for f in data.get("findings", []) if isinstance(data, dict) else []:
|
|
50
|
+
fp = f.get("file", "")
|
|
51
|
+
if not fp:
|
|
52
|
+
continue
|
|
53
|
+
per_file[fp] += 1
|
|
54
|
+
sev = f.get("severity", "low")
|
|
55
|
+
if order.get(sev, 0) > order.get(sev_max.get(fp, "info"), 0):
|
|
56
|
+
sev_max[fp] = sev
|
|
57
|
+
|
|
58
|
+
# file sizes for complexity hint
|
|
59
|
+
from patchi.core.brain.file_corpus import FileCorpus
|
|
60
|
+
|
|
61
|
+
corpus = FileCorpus(root)
|
|
62
|
+
size_map = {e.path: e.size_bytes for e in corpus.files()}
|
|
63
|
+
|
|
64
|
+
# build tree
|
|
65
|
+
tree: dict = {}
|
|
66
|
+
for rel, cnt in per_file.items():
|
|
67
|
+
parts = rel.split("/")
|
|
68
|
+
cur = tree
|
|
69
|
+
for seg in parts[:-1]:
|
|
70
|
+
cur = cur.setdefault(seg, {})
|
|
71
|
+
leaf = parts[-1]
|
|
72
|
+
churn = _git_churn(root, rel) if cnt else 0
|
|
73
|
+
size_kb = size_map.get(rel, 0) / 1024
|
|
74
|
+
score = cnt * 10 + churn * 2 + size_kb / 10
|
|
75
|
+
cur[leaf] = {"_value": round(score, 1), "_severity": sev_max.get(rel, "low"), "_findings": cnt}
|
|
76
|
+
|
|
77
|
+
def to_children(d: dict, name: str = "root") -> dict:
|
|
78
|
+
children = []
|
|
79
|
+
for k, v in sorted(d.items()):
|
|
80
|
+
if isinstance(v, dict) and "_value" in v:
|
|
81
|
+
children.append({"name": k, "value": v["_value"], "severity": v["_severity"], "findings": v["_findings"]})
|
|
82
|
+
elif isinstance(v, dict):
|
|
83
|
+
sub = to_children(v, k)
|
|
84
|
+
if sub["children"]:
|
|
85
|
+
total = sum(c.get("value", 0) for c in sub["children"])
|
|
86
|
+
sub["value"] = round(total, 1)
|
|
87
|
+
children.append(sub)
|
|
88
|
+
return {"name": name, "children": children}
|
|
89
|
+
|
|
90
|
+
root_node = to_children(tree)
|
|
91
|
+
# top hotspots
|
|
92
|
+
flat = []
|
|
93
|
+
|
|
94
|
+
def collect(node, prefix=""):
|
|
95
|
+
for ch in node.get("children", []):
|
|
96
|
+
path = f"{prefix}/{ch['name']}" if prefix else ch["name"]
|
|
97
|
+
if "children" in ch:
|
|
98
|
+
collect(ch, path)
|
|
99
|
+
else:
|
|
100
|
+
flat.append((path, ch["value"], ch["severity"]))
|
|
101
|
+
|
|
102
|
+
collect(root_node)
|
|
103
|
+
flat.sort(key=lambda x: -x[1])
|
|
104
|
+
return {"tree": root_node, "hotspots": [{"file": p, "score": s, "severity": sev} for p, s, sev in flat[:20]], "total_files": len(per_file)}
|