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,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layered Brain — the "complex brain system" for Patchi.
|
|
3
|
+
|
|
4
|
+
Breaks a codebase into persistent **layers** so the agent can retrieve context
|
|
5
|
+
for one part without re-reading the whole repository:
|
|
6
|
+
|
|
7
|
+
Layer 4: PROJECT → purpose, stack, conventions
|
|
8
|
+
Layer 3: FLOWS → critical paths (contract flows)
|
|
9
|
+
Layer 2: SUBSYSTEMS → "auth", "data", "api", "ui" (semantic groups)
|
|
10
|
+
Layer 1: MODULES → directory-level units (cached summaries)
|
|
11
|
+
Layer 0: FILES → FileInfo (handled elsewhere)
|
|
12
|
+
|
|
13
|
+
Each layer carries a cached heuristic summary, its inter-layer dependencies
|
|
14
|
+
(derived from the import graph), its public API, and a validity hash. When a
|
|
15
|
+
single file changes, only its module summary is recomputed and the change
|
|
16
|
+
propagates up one level — sibling layers are never touched.
|
|
17
|
+
|
|
18
|
+
This module is **additive**: it builds on the existing FileInfo / ImportGraph /
|
|
19
|
+
RouteInfo structures and serialises to `.patchi/memory/layers.json`. No rewrite
|
|
20
|
+
of the scan pipeline is required.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from patchi.core.brain.framework import StackInfo
|
|
32
|
+
from patchi.core.brain.import_graph import ImportGraph
|
|
33
|
+
from patchi.core.brain.route_mapper import RouteInfo
|
|
34
|
+
from patchi.core.brain.scanner import FileInfo
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Subsystem classification ──────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
# Maps a keyword (matched against module path) to a semantic subsystem name.
|
|
40
|
+
_SUBSYSTEM_RULES: list[tuple[str, str]] = [
|
|
41
|
+
(r"auth|login|session|security|oauth|jwt|token|passwd|permission", "auth"),
|
|
42
|
+
(r"api|route|router|controller|endpoint|view|handler|resource", "api"),
|
|
43
|
+
(r"model|db|data|repo|repository|orm|migration|schema|query|sql", "data"),
|
|
44
|
+
(r"test|spec|__tests__|e2e|fixture", "tests"),
|
|
45
|
+
(r"ui|component|page|frontend|web|dom|css|react|vue|svelte", "ui"),
|
|
46
|
+
(r"agent|scanner|fix|brain|analy|guard|detect", "agents"),
|
|
47
|
+
(r"core|util|common|lib|shared|helper|internal", "core"),
|
|
48
|
+
(r"config|deploy|infra|ci|docker|k8s|pipeline|script", "infra"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _classify_subsystem(module_name: str) -> str:
|
|
53
|
+
"""Map a module path to a semantic subsystem name."""
|
|
54
|
+
low = module_name.lower()
|
|
55
|
+
for pattern, name in _SUBSYSTEM_RULES:
|
|
56
|
+
if re.search(pattern, low):
|
|
57
|
+
return name
|
|
58
|
+
# Fall back to the top-level directory name
|
|
59
|
+
return module_name.split("/")[0] or "root"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Layer dataclass ───────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Layer:
|
|
67
|
+
"""A single layer in the Layered Brain."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
level: int # 1=module, 2=subsystem, 4=project
|
|
71
|
+
path: str = "" # relative path (modules) or logical name (subsystems/project)
|
|
72
|
+
language: str = "" # primary language of the layer
|
|
73
|
+
summary: str = "" # cached heuristic description
|
|
74
|
+
purpose: str = "" # one-line purpose (heuristic)
|
|
75
|
+
files: list[str] = field(default_factory=list) # child file paths
|
|
76
|
+
child_layers: list[str] = field(default_factory=list) # child layer names
|
|
77
|
+
parent_layer: str | None = None
|
|
78
|
+
depends_on: list[str] = field(default_factory=list) # layer names this imports
|
|
79
|
+
dependents: list[str] = field(default_factory=list) # layer names importing this
|
|
80
|
+
public_api: list[str] = field(default_factory=list) # exported symbols
|
|
81
|
+
validity_hash: str = "" # hash of public_api + summary + files
|
|
82
|
+
stale: bool = False # True when a parent/child change invalidated this layer
|
|
83
|
+
|
|
84
|
+
def compute_hash(self) -> str:
|
|
85
|
+
payload = (
|
|
86
|
+
self.name
|
|
87
|
+
+ "|"
|
|
88
|
+
+ self.summary
|
|
89
|
+
+ "|"
|
|
90
|
+
+ ",".join(sorted(self.public_api))
|
|
91
|
+
+ "|"
|
|
92
|
+
+ ",".join(sorted(self.files))
|
|
93
|
+
)
|
|
94
|
+
return hashlib.sha1(payload.encode("utf-8")).hexdigest()[:12]
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
return {
|
|
98
|
+
"name": self.name,
|
|
99
|
+
"level": self.level,
|
|
100
|
+
"path": self.path,
|
|
101
|
+
"language": self.language,
|
|
102
|
+
"summary": self.summary,
|
|
103
|
+
"purpose": self.purpose,
|
|
104
|
+
"files": self.files,
|
|
105
|
+
"child_layers": self.child_layers,
|
|
106
|
+
"parent_layer": self.parent_layer,
|
|
107
|
+
"depends_on": self.depends_on,
|
|
108
|
+
"dependents": self.dependents,
|
|
109
|
+
"public_api": self.public_api,
|
|
110
|
+
"validity_hash": self.validity_hash,
|
|
111
|
+
"stale": self.stale,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, d: dict) -> Layer:
|
|
116
|
+
return cls(
|
|
117
|
+
name=d["name"],
|
|
118
|
+
level=d["level"],
|
|
119
|
+
path=d.get("path", ""),
|
|
120
|
+
language=d.get("language", ""),
|
|
121
|
+
summary=d.get("summary", ""),
|
|
122
|
+
purpose=d.get("purpose", ""),
|
|
123
|
+
files=d.get("files", []),
|
|
124
|
+
child_layers=d.get("child_layers", []),
|
|
125
|
+
parent_layer=d.get("parent_layer"),
|
|
126
|
+
depends_on=d.get("depends_on", []),
|
|
127
|
+
dependents=d.get("dependents", []),
|
|
128
|
+
public_api=d.get("public_api", []),
|
|
129
|
+
validity_hash=d.get("validity_hash", ""),
|
|
130
|
+
stale=d.get("stale", False),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _module_of(file_path: str) -> str:
|
|
138
|
+
"""Return the module (parent directory) for a file path."""
|
|
139
|
+
parts = file_path.split("/")
|
|
140
|
+
if len(parts) <= 1:
|
|
141
|
+
return file_path # root-level file is its own module
|
|
142
|
+
return "/".join(parts[:-1])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _lang_value(lang) -> str:
|
|
146
|
+
if hasattr(lang, "value"):
|
|
147
|
+
return lang.value
|
|
148
|
+
return str(lang)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _module_summary(
|
|
152
|
+
module_name: str,
|
|
153
|
+
files: list[str],
|
|
154
|
+
fi_map: dict[str, FileInfo],
|
|
155
|
+
routes: list[RouteInfo],
|
|
156
|
+
) -> tuple[str, str, list[str]]:
|
|
157
|
+
"""Produce (summary, purpose, public_api) for a module."""
|
|
158
|
+
langs: dict[str, int] = {}
|
|
159
|
+
func_count = 0
|
|
160
|
+
class_count = 0
|
|
161
|
+
public_api: list[str] = []
|
|
162
|
+
for fp in files:
|
|
163
|
+
fi = fi_map.get(fp)
|
|
164
|
+
if not fi:
|
|
165
|
+
continue
|
|
166
|
+
lv = _lang_value(fi.language)
|
|
167
|
+
langs[lv] = langs.get(lv, 0) + 1
|
|
168
|
+
func_count += len(fi.functions)
|
|
169
|
+
class_count += len(fi.classes)
|
|
170
|
+
for fn in fi.functions:
|
|
171
|
+
public_api.append(fn.name)
|
|
172
|
+
for cl in fi.classes:
|
|
173
|
+
public_api.append(cl.name)
|
|
174
|
+
|
|
175
|
+
module_routes = [r for r in routes if _module_of(r.file) == module_name]
|
|
176
|
+
route_bits: list[str] = []
|
|
177
|
+
for r in module_routes[:6]:
|
|
178
|
+
method = r.method if hasattr(r, "method") else r.get("method", "GET")
|
|
179
|
+
path = r.path if hasattr(r, "path") else r.get("path", "/")
|
|
180
|
+
route_bits.append(f"{method} {path}")
|
|
181
|
+
route_summary = (" Routes: " + ", ".join(route_bits)) if route_bits else ""
|
|
182
|
+
|
|
183
|
+
lang_str = ", ".join(f"{k}×{v}" for k, v in langs.items()) or "unknown"
|
|
184
|
+
summary = (
|
|
185
|
+
f"Module '{module_name}' — {len(files)} file(s) ({lang_str}), "
|
|
186
|
+
f"{func_count} function(s), {class_count} class(es).{route_summary}"
|
|
187
|
+
)
|
|
188
|
+
purpose = f"Contains {func_count} function(s) and {class_count} class(es) under {module_name}."
|
|
189
|
+
return summary, purpose, public_api
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── Builder ───────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def build_layers(
|
|
196
|
+
file_infos: list[FileInfo],
|
|
197
|
+
graph: ImportGraph | None = None,
|
|
198
|
+
routes: list[RouteInfo] | None = None,
|
|
199
|
+
stack: StackInfo | None = None,
|
|
200
|
+
) -> dict[str, Layer]:
|
|
201
|
+
"""Build the full layered brain from scan results.
|
|
202
|
+
|
|
203
|
+
Returns a dict keyed by layer name. Includes module (L1), subsystem (L2),
|
|
204
|
+
and project (L4) layers. Layer 0 (individual files) is intentionally not
|
|
205
|
+
stored — it is the FileInfo cache already maintained by the scanner.
|
|
206
|
+
"""
|
|
207
|
+
routes = routes or []
|
|
208
|
+
fi_map = {fi.path: fi for fi in file_infos}
|
|
209
|
+
|
|
210
|
+
# ── Level 1: modules (one per directory) ──────────────────────────────────
|
|
211
|
+
modules: dict[str, list[str]] = {}
|
|
212
|
+
for fi in file_infos:
|
|
213
|
+
mod = _module_of(fi.path)
|
|
214
|
+
modules.setdefault(mod, []).append(fi.path)
|
|
215
|
+
|
|
216
|
+
layers: dict[str, Layer] = {}
|
|
217
|
+
for mod_name, files in modules.items():
|
|
218
|
+
summary, purpose, public_api = _module_summary(mod_name, files, fi_map, routes)
|
|
219
|
+
layer = Layer(
|
|
220
|
+
name=mod_name,
|
|
221
|
+
level=1,
|
|
222
|
+
path=mod_name,
|
|
223
|
+
language=_dominant_language(files, fi_map),
|
|
224
|
+
summary=summary,
|
|
225
|
+
purpose=purpose,
|
|
226
|
+
files=sorted(files),
|
|
227
|
+
public_api=sorted(set(public_api)),
|
|
228
|
+
)
|
|
229
|
+
layer.validity_hash = layer.compute_hash()
|
|
230
|
+
layers[mod_name] = layer
|
|
231
|
+
|
|
232
|
+
# ── Level 2: subsystems (cluster modules by semantic role) ─────────────────
|
|
233
|
+
subsystem_modules: dict[str, list[str]] = {}
|
|
234
|
+
module_to_subsystem: dict[str, str] = {}
|
|
235
|
+
for mod_name in modules:
|
|
236
|
+
sub = _classify_subsystem(mod_name)
|
|
237
|
+
subsystem_modules.setdefault(sub, []).append(mod_name)
|
|
238
|
+
module_to_subsystem[mod_name] = sub
|
|
239
|
+
|
|
240
|
+
for sub_name, mods in subsystem_modules.items():
|
|
241
|
+
sub_files: list[str] = []
|
|
242
|
+
sub_api: list[str] = []
|
|
243
|
+
for m in mods:
|
|
244
|
+
sub_files.extend(layers[m].files)
|
|
245
|
+
sub_api.extend(layers[m].public_api)
|
|
246
|
+
sub_summary = (
|
|
247
|
+
f"Subsystem '{sub_name}' — {len(mods)} module(s), "
|
|
248
|
+
f"{len(sub_files)} file(s). Modules: {', '.join(mods[:8])}."
|
|
249
|
+
)
|
|
250
|
+
sub_layer = Layer(
|
|
251
|
+
name=sub_name,
|
|
252
|
+
level=2,
|
|
253
|
+
path=sub_name,
|
|
254
|
+
language=_dominant_language(sub_files, fi_map),
|
|
255
|
+
summary=sub_summary,
|
|
256
|
+
purpose=f"Groups {len(mods)} module(s) under the '{sub_name}' concern.",
|
|
257
|
+
files=sorted(set(sub_files)),
|
|
258
|
+
child_layers=sorted(mods),
|
|
259
|
+
public_api=sorted(set(sub_api)),
|
|
260
|
+
)
|
|
261
|
+
sub_layer.validity_hash = sub_layer.compute_hash()
|
|
262
|
+
layers[sub_name] = sub_layer
|
|
263
|
+
for m in mods:
|
|
264
|
+
layers[m].parent_layer = sub_name
|
|
265
|
+
|
|
266
|
+
# ── Inter-layer dependencies (from import graph) ───────────────────────────
|
|
267
|
+
if graph is not None:
|
|
268
|
+
_populate_dependencies(layers, graph, module_to_subsystem)
|
|
269
|
+
|
|
270
|
+
# ── Level 4: project ───────────────────────────────────────────────────────
|
|
271
|
+
fw_names = (
|
|
272
|
+
", ".join(f.name for f in stack.frameworks[:5]) if stack and stack.frameworks else "unknown"
|
|
273
|
+
)
|
|
274
|
+
runtime = stack.runtime if stack else "unknown"
|
|
275
|
+
project_summary = (
|
|
276
|
+
f"Project — {len(file_infos)} file(s) across {len(modules)} module(s) "
|
|
277
|
+
f"and {len(subsystem_modules)} subsystem(s). "
|
|
278
|
+
f"Stack: {fw_names} (runtime: {runtime})."
|
|
279
|
+
)
|
|
280
|
+
project_layer = Layer(
|
|
281
|
+
name="__project__",
|
|
282
|
+
level=4,
|
|
283
|
+
path="",
|
|
284
|
+
language=runtime,
|
|
285
|
+
summary=project_summary,
|
|
286
|
+
purpose="The whole project — top of the layer hierarchy.",
|
|
287
|
+
files=sorted(fi.path for fi in file_infos),
|
|
288
|
+
child_layers=sorted(subsystem_modules.keys()),
|
|
289
|
+
public_api=sorted({a for lay in layers.values() for a in lay.public_api}),
|
|
290
|
+
)
|
|
291
|
+
project_layer.validity_hash = project_layer.compute_hash()
|
|
292
|
+
layers["__project__"] = project_layer
|
|
293
|
+
for sub in subsystem_modules:
|
|
294
|
+
layers[sub].parent_layer = "__project__"
|
|
295
|
+
|
|
296
|
+
return layers
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _dominant_language(files: list[str], fi_map: dict[str, FileInfo]) -> str:
|
|
300
|
+
counts: dict[str, int] = {}
|
|
301
|
+
for fp in files:
|
|
302
|
+
fi = fi_map.get(fp)
|
|
303
|
+
if fi:
|
|
304
|
+
lv = _lang_value(fi.language)
|
|
305
|
+
counts[lv] = counts.get(lv, 0) + 1
|
|
306
|
+
if not counts:
|
|
307
|
+
return ""
|
|
308
|
+
return max(counts, key=counts.get)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _populate_dependencies(
|
|
312
|
+
layers: dict[str, Layer],
|
|
313
|
+
graph: ImportGraph,
|
|
314
|
+
module_to_subsystem: dict[str, str],
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Derive module- and subsystem-level dependency edges from the graph."""
|
|
317
|
+
edges: dict[str, set[str]] = {}
|
|
318
|
+
raw_edges: dict[str, set[str]] = {}
|
|
319
|
+
if hasattr(graph, "edges"):
|
|
320
|
+
raw_edges = graph.edges
|
|
321
|
+
elif isinstance(graph, dict) and "edges" in graph:
|
|
322
|
+
raw_edges = graph["edges"]
|
|
323
|
+
|
|
324
|
+
for src_file, targets in raw_edges.items():
|
|
325
|
+
src_mod = _module_of(src_file)
|
|
326
|
+
for tgt in targets:
|
|
327
|
+
tgt_mod = _module_of(tgt) if isinstance(tgt, str) else str(tgt)
|
|
328
|
+
if src_mod == tgt_mod:
|
|
329
|
+
continue
|
|
330
|
+
edges.setdefault(src_mod, set()).add(tgt_mod)
|
|
331
|
+
|
|
332
|
+
# Module-level edges
|
|
333
|
+
for src_mod, tgts in edges.items():
|
|
334
|
+
if src_mod not in layers:
|
|
335
|
+
continue
|
|
336
|
+
for tgt_mod in tgts:
|
|
337
|
+
if tgt_mod in layers:
|
|
338
|
+
if tgt_mod not in layers[src_mod].depends_on:
|
|
339
|
+
layers[src_mod].depends_on.append(tgt_mod)
|
|
340
|
+
if src_mod not in layers[tgt_mod].dependents:
|
|
341
|
+
layers[tgt_mod].dependents.append(src_mod)
|
|
342
|
+
|
|
343
|
+
# Subsystem-level edges (cluster module edges)
|
|
344
|
+
sub_edges: dict[str, set[str]] = {}
|
|
345
|
+
for src_mod, tgts in edges.items():
|
|
346
|
+
src_sub = module_to_subsystem.get(src_mod)
|
|
347
|
+
if not src_sub:
|
|
348
|
+
continue
|
|
349
|
+
for tgt_mod in tgts:
|
|
350
|
+
tgt_sub = module_to_subsystem.get(tgt_mod)
|
|
351
|
+
if not tgt_sub or tgt_sub == src_sub:
|
|
352
|
+
continue
|
|
353
|
+
sub_edges.setdefault(src_sub, set()).add(tgt_sub)
|
|
354
|
+
|
|
355
|
+
for src_sub, tgts in sub_edges.items():
|
|
356
|
+
if src_sub not in layers:
|
|
357
|
+
continue
|
|
358
|
+
for tgt_sub in sorted(tgts):
|
|
359
|
+
if tgt_sub in layers:
|
|
360
|
+
if tgt_sub not in layers[src_sub].depends_on:
|
|
361
|
+
layers[src_sub].depends_on.append(tgt_sub)
|
|
362
|
+
if src_sub not in layers[tgt_sub].dependents:
|
|
363
|
+
layers[tgt_sub].dependents.append(src_sub)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ── Serialisation helpers ─────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def layers_to_dict(layers: dict[str, Layer]) -> dict:
|
|
370
|
+
return {
|
|
371
|
+
"version": 1,
|
|
372
|
+
"layer_count": len(layers),
|
|
373
|
+
"layers": {name: layer.to_dict() for name, layer in layers.items()},
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def layers_from_dict(data: dict) -> dict[str, Layer]:
|
|
378
|
+
return {name: Layer.from_dict(d) for name, d in data.get("layers", {}).items()}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ── Query helpers (used by the reasoning engine in later phases) ───────────────
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def get_layer_context(layers: dict[str, Layer], layer_name: str, max_depth: int = 1) -> dict:
|
|
385
|
+
"""Return a layer plus its direct dependency summaries for fast context."""
|
|
386
|
+
if layer_name not in layers:
|
|
387
|
+
return {}
|
|
388
|
+
out: dict[str, str] = {}
|
|
389
|
+
seen: set[str] = set()
|
|
390
|
+
|
|
391
|
+
def collect(name: str, depth: int) -> None:
|
|
392
|
+
if name in seen or depth > max_depth:
|
|
393
|
+
return
|
|
394
|
+
seen.add(name)
|
|
395
|
+
layer = layers.get(name)
|
|
396
|
+
if not layer:
|
|
397
|
+
return
|
|
398
|
+
out[name] = layer.summary
|
|
399
|
+
if depth < max_depth:
|
|
400
|
+
for dep in layer.depends_on:
|
|
401
|
+
collect(dep, depth + 1)
|
|
402
|
+
|
|
403
|
+
collect(layer_name, 0)
|
|
404
|
+
return out
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def find_layers_for_file(layers: dict[str, Layer], file_path: str) -> list[str]:
|
|
408
|
+
"""Return the names of layers that contain a given file."""
|
|
409
|
+
return [name for name, layer in layers.items() if file_path in layer.files]
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def detect_stale_layers(old_layers: dict[str, Layer], new_layers: dict[str, Layer]) -> list[str]:
|
|
413
|
+
"""Compare two layer sets and return names whose hash changed."""
|
|
414
|
+
stale: list[str] = []
|
|
415
|
+
for name, layer in new_layers.items():
|
|
416
|
+
old = old_layers.get(name)
|
|
417
|
+
if old is None or old.validity_hash != layer.validity_hash:
|
|
418
|
+
stale.append(name)
|
|
419
|
+
return stale
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Learning Brain — tracks user accept/reject patterns to improve future suggestions.
|
|
3
|
+
|
|
4
|
+
Maintains a Bayesian-style classifier that learns:
|
|
5
|
+
- Which finding types the user always accepts → suggest more aggressively
|
|
6
|
+
- Which finding types the user always rejects → stop suggesting
|
|
7
|
+
- Which fix agents produce patches the user trusts → prioritize those
|
|
8
|
+
|
|
9
|
+
All data stored in .patchi/learning.json. Zero external dependencies.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
_LEARNING_FILE = ".patchi/learning.json"
|
|
19
|
+
|
|
20
|
+
# Minimum samples before we trust the pattern
|
|
21
|
+
_MIN_SAMPLES = 3
|
|
22
|
+
|
|
23
|
+
# Decay factor: older decisions matter less (per day)
|
|
24
|
+
_DECAY_HALF_LIFE_DAYS = 30
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_log = logging.getLogger("patchi.brain.learning")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load(root: Path) -> dict:
|
|
31
|
+
path = root / _LEARNING_FILE
|
|
32
|
+
if not path.exists():
|
|
33
|
+
return {"acceptances": {}, "rejections": {}, "agent_trust": {}}
|
|
34
|
+
try:
|
|
35
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
36
|
+
except Exception as e:
|
|
37
|
+
_log.warning("_load failed: %s", e)
|
|
38
|
+
return {"acceptances": {}, "rejections": {}, "agent_trust": {}}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save(root: Path, data: dict) -> None:
|
|
42
|
+
path = root / _LEARNING_FILE
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
tmp = path.with_suffix(".tmp")
|
|
45
|
+
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
46
|
+
tmp.replace(path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def record_acceptance(finding_type: str, agent_name: str, root: Path) -> None:
|
|
50
|
+
"""Record that the user accepted a fix for this finding type."""
|
|
51
|
+
data = _load(root)
|
|
52
|
+
now = time.time()
|
|
53
|
+
|
|
54
|
+
acceptances = data.setdefault("acceptances", {})
|
|
55
|
+
acceptances.setdefault(finding_type, []).append(now)
|
|
56
|
+
# Keep last 100 entries per type
|
|
57
|
+
acceptances[finding_type] = acceptances[finding_type][-100:]
|
|
58
|
+
|
|
59
|
+
trust = data.setdefault("agent_trust", {})
|
|
60
|
+
trust.setdefault(agent_name, {"accepts": 0, "rejects": 0})
|
|
61
|
+
trust[agent_name]["accepts"] += 1
|
|
62
|
+
|
|
63
|
+
_save(root, data)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def record_rejection(finding_type: str, agent_name: str, root: Path) -> None:
|
|
67
|
+
"""Record that the user rejected a fix for this finding type."""
|
|
68
|
+
data = _load(root)
|
|
69
|
+
now = time.time()
|
|
70
|
+
|
|
71
|
+
rejections = data.setdefault("rejections", {})
|
|
72
|
+
rejections.setdefault(finding_type, []).append(now)
|
|
73
|
+
rejections[finding_type] = rejections[finding_type][-100:]
|
|
74
|
+
|
|
75
|
+
trust = data.setdefault("agent_trust", {})
|
|
76
|
+
trust.setdefault(agent_name, {"accepts": 0, "rejects": 0})
|
|
77
|
+
trust[agent_name]["rejects"] += 1
|
|
78
|
+
|
|
79
|
+
_save(root, data)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def should_suggest(finding_type: str, root: Path) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Should we suggest a fix for this finding type?
|
|
85
|
+
Returns False if the user has rejected this type 3+ times more than they accepted it.
|
|
86
|
+
"""
|
|
87
|
+
data = _load(root)
|
|
88
|
+
acceptances = data.get("acceptances", {}).get(finding_type, [])
|
|
89
|
+
rejections = data.get("rejections", {}).get(finding_type, [])
|
|
90
|
+
|
|
91
|
+
total = len(acceptances) + len(rejections)
|
|
92
|
+
if total < _MIN_SAMPLES:
|
|
93
|
+
return True # Not enough data yet
|
|
94
|
+
|
|
95
|
+
# Weighted by recency
|
|
96
|
+
now = time.time()
|
|
97
|
+
accept_score = _recency_weighted(acceptances, now)
|
|
98
|
+
reject_score = _recency_weighted(rejections, now)
|
|
99
|
+
|
|
100
|
+
# If rejections outweigh acceptances 3:1, stop suggesting
|
|
101
|
+
if reject_score > accept_score * 3:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_agent_trust(agent_name: str, root: Path) -> float:
|
|
108
|
+
"""
|
|
109
|
+
Get trust score for an agent (0.0 to 1.0).
|
|
110
|
+
1.0 = user always accepts, 0.0 = user always rejects.
|
|
111
|
+
"""
|
|
112
|
+
data = _load(root)
|
|
113
|
+
trust = data.get("agent_trust", {}).get(agent_name, {"accepts": 0, "rejects": 0})
|
|
114
|
+
total = trust["accepts"] + trust["rejects"]
|
|
115
|
+
if total < _MIN_SAMPLES:
|
|
116
|
+
return 0.5 # Neutral until we have data
|
|
117
|
+
return trust["accepts"] / total
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_summary(root: Path) -> dict:
|
|
121
|
+
"""Return a summary of learned patterns."""
|
|
122
|
+
data = _load(root)
|
|
123
|
+
time.time()
|
|
124
|
+
|
|
125
|
+
summary = {
|
|
126
|
+
"acceptances": {},
|
|
127
|
+
"rejections": {},
|
|
128
|
+
"agent_trust": {},
|
|
129
|
+
"skip_types": [],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for ftype, timestamps in data.get("acceptances", {}).items():
|
|
133
|
+
summary["acceptances"][ftype] = len(timestamps)
|
|
134
|
+
|
|
135
|
+
for ftype, timestamps in data.get("rejections", {}).items():
|
|
136
|
+
summary["rejections"][ftype] = len(timestamps)
|
|
137
|
+
|
|
138
|
+
for agent, scores in data.get("agent_trust", {}).items():
|
|
139
|
+
total = scores["accepts"] + scores["rejects"]
|
|
140
|
+
if total >= _MIN_SAMPLES:
|
|
141
|
+
summary["agent_trust"][agent] = round(scores["accepts"] / total, 2)
|
|
142
|
+
|
|
143
|
+
# Find types that should be skipped
|
|
144
|
+
for ftype in set(
|
|
145
|
+
list(data.get("acceptances", {}).keys()) + list(data.get("rejections", {}).keys())
|
|
146
|
+
):
|
|
147
|
+
if not should_suggest(ftype, root):
|
|
148
|
+
summary["skip_types"].append(ftype)
|
|
149
|
+
|
|
150
|
+
return summary
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── Fix-pattern memory ─────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def record_fix_pattern(
|
|
157
|
+
finding_type: str,
|
|
158
|
+
file_pattern: str,
|
|
159
|
+
fix_strategy: str,
|
|
160
|
+
fix_summary: str,
|
|
161
|
+
root: Path,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Record a successful fix pattern so it can be reused for similar findings."""
|
|
164
|
+
data = _load(root)
|
|
165
|
+
patterns = data.setdefault("fix_patterns", [])
|
|
166
|
+
patterns.append(
|
|
167
|
+
{
|
|
168
|
+
"finding_type": finding_type,
|
|
169
|
+
"file_pattern": file_pattern,
|
|
170
|
+
"fix_strategy": fix_strategy,
|
|
171
|
+
"fix_summary": fix_summary,
|
|
172
|
+
"timestamp": time.time(),
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
# Keep last 500 patterns to avoid unbounded growth
|
|
176
|
+
data["fix_patterns"] = patterns[-500:]
|
|
177
|
+
_save(root, data)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_fix_pattern(finding_type: str, file_path: str, root: Path) -> dict | None:
|
|
181
|
+
"""Find the best matching fix pattern for a given finding type + file path.
|
|
182
|
+
|
|
183
|
+
Matches by finding_type first, then by file extension pattern.
|
|
184
|
+
Returns the most recent match, or None.
|
|
185
|
+
"""
|
|
186
|
+
data = _load(root)
|
|
187
|
+
patterns = data.get("fix_patterns", [])
|
|
188
|
+
if not patterns:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
ext = Path(file_path).suffix if file_path else ""
|
|
192
|
+
# Score each pattern: exact type match + optional ext match
|
|
193
|
+
scored: list[tuple[float, dict]] = []
|
|
194
|
+
for p in patterns:
|
|
195
|
+
score = 0.0
|
|
196
|
+
if p.get("finding_type") == finding_type:
|
|
197
|
+
score += 2.0
|
|
198
|
+
if ext and p.get("file_pattern", "").endswith(ext):
|
|
199
|
+
score += 1.0
|
|
200
|
+
if score > 0:
|
|
201
|
+
# Recency bonus: patterns applied within last 7 days get +1
|
|
202
|
+
age = time.time() - p.get("timestamp", 0)
|
|
203
|
+
if age < 604800:
|
|
204
|
+
score += 1.0
|
|
205
|
+
scored.append((score, p))
|
|
206
|
+
|
|
207
|
+
if not scored:
|
|
208
|
+
return None
|
|
209
|
+
scored.sort(key=lambda x: -x[0])
|
|
210
|
+
return scored[0][1]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def list_fix_patterns(root: Path, limit: int = 20) -> list[dict]:
|
|
214
|
+
"""Return recent fix patterns for display."""
|
|
215
|
+
data = _load(root)
|
|
216
|
+
patterns = data.get("fix_patterns", [])
|
|
217
|
+
return patterns[-limit:]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _recency_weighted(timestamps: list[float], now: float) -> float:
|
|
221
|
+
"""Weight timestamps by recency using exponential decay."""
|
|
222
|
+
import math
|
|
223
|
+
|
|
224
|
+
score = 0.0
|
|
225
|
+
for ts in timestamps:
|
|
226
|
+
days_ago = (now - ts) / 86400
|
|
227
|
+
weight = math.exp(-0.693 * days_ago / _DECAY_HALF_LIFE_DAYS)
|
|
228
|
+
score += weight
|
|
229
|
+
return score
|