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,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enriched Brain Context — Slice 1 of Smart Brain v2.
|
|
3
|
+
|
|
4
|
+
Single AI call per scan that turns deterministic signals
|
|
5
|
+
(project_reader + stack + routes + domains) into a short,
|
|
6
|
+
structured reasoning object stored in brain.json.
|
|
7
|
+
|
|
8
|
+
Design constraints (from deep audit):
|
|
9
|
+
- Exactly ONE LLM call per scan (not per finding)
|
|
10
|
+
- Honors PATCHI_OFFLINE=1 → no network
|
|
11
|
+
- Timeout-bound (8s default), never blocks scan
|
|
12
|
+
- Offline fallback builds same schema from heuristics
|
|
13
|
+
- Result is additive: brain.json["enriched_context"]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
_log = logging.getLogger("patchi.brain.enriched_context")
|
|
24
|
+
|
|
25
|
+
DEFAULT_TIMEOUT = 8.0
|
|
26
|
+
MAX_CHARS = 6000
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def enrich_project_context(
|
|
30
|
+
root: Path,
|
|
31
|
+
config: dict[str, Any],
|
|
32
|
+
stack: Any,
|
|
33
|
+
routes: list[Any],
|
|
34
|
+
file_infos: list[Any],
|
|
35
|
+
context: dict[str, Any],
|
|
36
|
+
active_domains: list[str],
|
|
37
|
+
extra_context: dict[str, Any] | None = None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Build enriched_context. Always returns a dict with the same keys,
|
|
41
|
+
whether AI ran or we fell back to heuristics.
|
|
42
|
+
|
|
43
|
+
Keys: purpose_1sent, domain, domain_confidence, tech_stack_confirmed,
|
|
44
|
+
critical_dirs_reasoned, top_risks[3], scan_focus
|
|
45
|
+
"""
|
|
46
|
+
fallback = _heuristic_enrichment(stack, routes, context, active_domains)
|
|
47
|
+
if os.environ.get("PATCHI_OFFLINE"):
|
|
48
|
+
_log.debug("enriched_context: offline mode — heuristic fallback")
|
|
49
|
+
fallback["source"] = "heuristic_offline"
|
|
50
|
+
return fallback
|
|
51
|
+
|
|
52
|
+
prompt = _build_prompt(stack, routes, file_infos, context, active_domains, extra_context or {})
|
|
53
|
+
system = (
|
|
54
|
+
"You are Patchi Context Synthesizer. Given deterministic signals about a codebase, "
|
|
55
|
+
"produce a short, structured assessment. Be precise and conservative. "
|
|
56
|
+
"Do not invent frameworks or domains not present in the signals. "
|
|
57
|
+
"Return ONLY valid JSON with exactly these keys: "
|
|
58
|
+
'{"purpose_1sent": str, "domain": str, "domain_confidence": float 0-1, '
|
|
59
|
+
'"tech_stack_confirmed": [str], "critical_dirs_reasoned": [{"dir": str, "why": str}], '
|
|
60
|
+
'"top_risks": [{"risk": str, "why": str}], "scan_focus": str}'
|
|
61
|
+
)
|
|
62
|
+
try:
|
|
63
|
+
from patchi.core.ai.client import call_ai_structured
|
|
64
|
+
|
|
65
|
+
result = call_ai_structured(
|
|
66
|
+
config,
|
|
67
|
+
system,
|
|
68
|
+
prompt,
|
|
69
|
+
max_tokens=800,
|
|
70
|
+
temperature=0.1,
|
|
71
|
+
)
|
|
72
|
+
if isinstance(result, dict) and "purpose_1sent" in result:
|
|
73
|
+
_log.info("enriched_context: AI succeeded (domain=%s)", result.get("domain"))
|
|
74
|
+
normalized = _normalize(result, fallback)
|
|
75
|
+
normalized["source"] = "ai_enriched"
|
|
76
|
+
return normalized
|
|
77
|
+
_log.debug("enriched_context: AI returned empty/unexpected shape, using heuristic")
|
|
78
|
+
except Exception as exc: # noqa: BLE001
|
|
79
|
+
_log.warning("enriched_context AI call failed, heuristic fallback: %s", exc)
|
|
80
|
+
fallback["source"] = "heuristic_ai_unavailable"
|
|
81
|
+
return fallback
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_prompt(
|
|
85
|
+
stack: Any,
|
|
86
|
+
routes: list[Any],
|
|
87
|
+
file_infos: list[Any],
|
|
88
|
+
context: dict[str, Any],
|
|
89
|
+
active_domains: list[str],
|
|
90
|
+
extra_context: dict[str, Any] | None = None,
|
|
91
|
+
) -> str:
|
|
92
|
+
# StackInfo
|
|
93
|
+
fws = [f.name for f in (stack.frameworks if stack and getattr(stack, "frameworks", None) else [])]
|
|
94
|
+
fw_str = ", ".join(fws) if fws else "Unknown"
|
|
95
|
+
runtime = getattr(stack, "runtime", "") if stack else ""
|
|
96
|
+
has_ts = getattr(stack, "has_typescript", False) if stack else False
|
|
97
|
+
langs = {}
|
|
98
|
+
for fi in file_infos:
|
|
99
|
+
try:
|
|
100
|
+
lang = fi.language.value if hasattr(fi.language, "value") else str(fi.language)
|
|
101
|
+
except Exception:
|
|
102
|
+
lang = "unknown"
|
|
103
|
+
langs[lang] = langs.get(lang, 0) + 1
|
|
104
|
+
lang_str = ", ".join(f"{k}:{v}" for k, v in sorted(langs.items(), key=lambda x: -x[1])[:5])
|
|
105
|
+
route_sample = [getattr(r, "path", str(r)) for r in routes[:12]]
|
|
106
|
+
infra = context.get("infrastructure_files", []) if isinstance(context, dict) else []
|
|
107
|
+
deps = context.get("dependencies", []) if isinstance(context, dict) else []
|
|
108
|
+
# ProjectInsight
|
|
109
|
+
insight_block = ""
|
|
110
|
+
try:
|
|
111
|
+
pi = extra_context.get("project_insight") if extra_context else None
|
|
112
|
+
if isinstance(pi, dict) and pi:
|
|
113
|
+
insight_block = f"\nPROJECT_INSIGHT: {pi.get('name','')} — {pi.get('description','')[:200]} | type={pi.get('project_type','')} fw={pi.get('framework','')} lang={pi.get('language','')} tech={pi.get('tech_stack',[])[:6]} entry={pi.get('entry_points',[])[:3]} readme={pi.get('readme_summary','')[:300]}\n"
|
|
114
|
+
except Exception:
|
|
115
|
+
insight_block = ""
|
|
116
|
+
# Layer summaries (up to 10)
|
|
117
|
+
layer_block = ""
|
|
118
|
+
try:
|
|
119
|
+
lb = extra_context.get("layer_summaries") if extra_context else None
|
|
120
|
+
if isinstance(lb, list) and lb:
|
|
121
|
+
layer_block = "\nLAYER_SUMMARIES:\n" + "\n".join(f"- {lay.get('name','')} ({lay.get('level','')}): {lay.get('summary','')[:180]}" for lay in lb[:10]) + "\n"
|
|
122
|
+
except Exception:
|
|
123
|
+
layer_block = ""
|
|
124
|
+
core_block = ""
|
|
125
|
+
if extra_context and extra_context.get("core_files_block"):
|
|
126
|
+
core_block = f"\nCORE_FILES (understander-ranked):\n{extra_context['core_files_block']}\n"
|
|
127
|
+
return (
|
|
128
|
+
f"FRAMEWORKS: {fw_str} runtime={runtime} ts={has_ts}\n"
|
|
129
|
+
f"LANGUAGES: {lang_str}\n"
|
|
130
|
+
f"FILE_COUNT: {len(file_infos)}\n"
|
|
131
|
+
f"ROUTES ({len(routes)}): {route_sample}\n"
|
|
132
|
+
f"ACTIVE_DOMAINS ({len(active_domains)}): {active_domains[:18]}\n"
|
|
133
|
+
f"DEPLOYMENT: {context.get('deployment_model', 'unknown') if isinstance(context, dict) else 'unknown'}\n"
|
|
134
|
+
f"INFRA_SAMPLE: {infra[:10]}\n"
|
|
135
|
+
f"DEPS_SAMPLE: {deps[:15]}\n"
|
|
136
|
+
f"CONTEXT_KEYS: {list(context.keys())[:14] if isinstance(context, dict) else []}\n"
|
|
137
|
+
+ insight_block
|
|
138
|
+
+ layer_block
|
|
139
|
+
+ core_block
|
|
140
|
+
+ "\nTask: synthesize. 1 sentence purpose, pick single domain label, confirm tech stack from signals, "
|
|
141
|
+
+ "name 2-4 critical dirs with why, list top 3 risks mapped to active domains, and one-line scan focus."
|
|
142
|
+
)[:MAX_CHARS]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _heuristic_enrichment(
|
|
146
|
+
stack: Any,
|
|
147
|
+
routes: list[Any],
|
|
148
|
+
context: dict[str, Any],
|
|
149
|
+
active_domains: list[str],
|
|
150
|
+
) -> dict[str, Any]:
|
|
151
|
+
fws = [f.name for f in (stack.frameworks if stack and getattr(stack, "frameworks", None) else [])]
|
|
152
|
+
fw_str = ", ".join(fws) if fws else "Unknown project"
|
|
153
|
+
has_routes = len(routes) > 0
|
|
154
|
+
# domain heuristic: most informative active domain or deployment
|
|
155
|
+
domain = active_domains[0] if active_domains else context.get("deployment_model", "unknown") if isinstance(context, dict) else "unknown"
|
|
156
|
+
if isinstance(domain, str) and "-" in domain:
|
|
157
|
+
pass # keep as-is
|
|
158
|
+
critical: list[dict[str, str]] = []
|
|
159
|
+
if has_routes:
|
|
160
|
+
critical.append({"dir": "routes/api", "why": f"{len(routes)} endpoints — request surface"})
|
|
161
|
+
if fws:
|
|
162
|
+
critical.append({"dir": fws[0].lower(), "why": f"primary framework {fws[0]}"})
|
|
163
|
+
# infer from context keys
|
|
164
|
+
if isinstance(context, dict):
|
|
165
|
+
if context.get("has_db") or "data_layer" in active_domains:
|
|
166
|
+
critical.append({"dir": "db/models", "why": "data layer detected"})
|
|
167
|
+
if "auth_session" in active_domains or "auth-session" in active_domains:
|
|
168
|
+
critical.append({"dir": "auth", "why": "auth/session domain active"})
|
|
169
|
+
|
|
170
|
+
risks: list[dict[str, str]] = []
|
|
171
|
+
for d in active_domains[:3]:
|
|
172
|
+
risks.append({"risk": d, "why": f"domain {d} activated — verify handlers"})
|
|
173
|
+
while len(risks) < 3:
|
|
174
|
+
risks.append({"risk": "supply-chain", "why": "dependencies present — pin & audit"})
|
|
175
|
+
if len(risks) >= 3:
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
domains_3 = ", ".join(active_domains[:3]) or "general"
|
|
179
|
+
domains_2 = ", ".join(active_domains[:2]) or "general security"
|
|
180
|
+
return {
|
|
181
|
+
"purpose_1sent": f"{fw_str} project with {len(routes)} routes; domains: {domains_3}",
|
|
182
|
+
"domain": domain if isinstance(domain, str) else str(domain),
|
|
183
|
+
"domain_confidence": 0.55 if active_domains else 0.35,
|
|
184
|
+
"tech_stack_confirmed": fws[:5],
|
|
185
|
+
"critical_dirs_reasoned": critical[:4],
|
|
186
|
+
"top_risks": risks[:3],
|
|
187
|
+
"scan_focus": f"Prioritize {domains_2}; {len(routes)} routes",
|
|
188
|
+
"source": "heuristic",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _normalize(ai_result: dict[str, Any], fallback: dict[str, Any]) -> dict[str, Any]:
|
|
193
|
+
out = dict(fallback)
|
|
194
|
+
for key in ("purpose_1sent", "domain", "scan_focus"):
|
|
195
|
+
val = ai_result.get(key)
|
|
196
|
+
if isinstance(val, str) and val.strip():
|
|
197
|
+
out[key] = val.strip()[:400]
|
|
198
|
+
dc = ai_result.get("domain_confidence")
|
|
199
|
+
if isinstance(dc, (int, float)) and 0 <= dc <= 1:
|
|
200
|
+
out["domain_confidence"] = float(dc)
|
|
201
|
+
tsc = ai_result.get("tech_stack_confirmed")
|
|
202
|
+
if isinstance(tsc, list):
|
|
203
|
+
out["tech_stack_confirmed"] = [str(x)[:40] for x in tsc[:8] if str(x).strip()]
|
|
204
|
+
cdr = ai_result.get("critical_dirs_reasoned")
|
|
205
|
+
if isinstance(cdr, list):
|
|
206
|
+
norm = []
|
|
207
|
+
for item in cdr[:4]:
|
|
208
|
+
if isinstance(item, dict) and item.get("dir"):
|
|
209
|
+
norm.append({"dir": str(item["dir"])[:60], "why": str(item.get("why", ""))[:120]})
|
|
210
|
+
if norm:
|
|
211
|
+
out["critical_dirs_reasoned"] = norm
|
|
212
|
+
tr = ai_result.get("top_risks")
|
|
213
|
+
if isinstance(tr, list):
|
|
214
|
+
norm = []
|
|
215
|
+
for item in tr[:3]:
|
|
216
|
+
if isinstance(item, dict) and item.get("risk"):
|
|
217
|
+
norm.append({"risk": str(item["risk"])[:60], "why": str(item.get("why", ""))[:160]})
|
|
218
|
+
if norm:
|
|
219
|
+
out["top_risks"] = norm
|
|
220
|
+
return out
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FileCorpus — parse-once file discovery layer for the Scan Bus.
|
|
3
|
+
|
|
4
|
+
Single-pass directory walker that:
|
|
5
|
+
- Respects DEFAULT_IGNORE_DIRS consistently (fixes the 62-file rglob scatter)
|
|
6
|
+
- Caches file content + parsed data for the duration of one scan
|
|
7
|
+
- Provides language-filtered query API
|
|
8
|
+
- Eliminates the O(n) redundant re-read pattern that caused the
|
|
9
|
+
proactive.py O(n²) bug
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
corpus = FileCorpus(project_root)
|
|
13
|
+
for fi in corpus.files():
|
|
14
|
+
process(fi)
|
|
15
|
+
for fi in corpus.by_language(Lang.PYTHON):
|
|
16
|
+
process_python(fi)
|
|
17
|
+
for fi in corpus.by_ext(".py"):
|
|
18
|
+
...
|
|
19
|
+
content = corpus.read(fi.path) # cached after first read
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from patchi.core.brain.languages import (
|
|
31
|
+
DEFAULT_IGNORE_DIRS,
|
|
32
|
+
Lang,
|
|
33
|
+
detect_language,
|
|
34
|
+
is_minified_asset,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_log = logging.getLogger("patchi.core.brain.file_corpus")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Defaults ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
_SKIP_DIRS = frozenset(DEFAULT_IGNORE_DIRS)
|
|
43
|
+
_SKIP_FILES: frozenset[str] = frozenset()
|
|
44
|
+
_MAX_FILE_SIZE = 2 * 1024 * 1024 # 2 MB
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Corpus entry ──────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class CorpusEntry:
|
|
52
|
+
path: str # relative to project root
|
|
53
|
+
abs_path: Path
|
|
54
|
+
language: Lang
|
|
55
|
+
size_bytes: int
|
|
56
|
+
_content: str | None = field(default=None, repr=False)
|
|
57
|
+
_content_hash: str | None = field(default=None, repr=False)
|
|
58
|
+
|
|
59
|
+
def read(self) -> str:
|
|
60
|
+
if self._content is None:
|
|
61
|
+
self._content = self.abs_path.read_text(encoding="utf-8", errors="replace")
|
|
62
|
+
return self._content
|
|
63
|
+
|
|
64
|
+
def content_hash(self) -> str:
|
|
65
|
+
if self._content_hash is None:
|
|
66
|
+
h = hashlib.md5()
|
|
67
|
+
with open(self.abs_path, "rb") as f:
|
|
68
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
69
|
+
h.update(chunk)
|
|
70
|
+
self._content_hash = h.hexdigest()
|
|
71
|
+
return self._content_hash
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── FileCorpus ────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class FileCorpus:
|
|
78
|
+
"""Single-pass, parse-once file index for a project root."""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
root: Path,
|
|
83
|
+
skip_dirs: frozenset[str] = _SKIP_DIRS,
|
|
84
|
+
skip_files: frozenset[str] = _SKIP_FILES,
|
|
85
|
+
max_size: int = _MAX_FILE_SIZE,
|
|
86
|
+
exclude_noise: bool = False,
|
|
87
|
+
exclude_tests: bool = False,
|
|
88
|
+
ignore_learner=None,
|
|
89
|
+
):
|
|
90
|
+
self.root = root.resolve()
|
|
91
|
+
self._skip_dirs = skip_dirs
|
|
92
|
+
self._skip_files = skip_files
|
|
93
|
+
self._max_size = max_size
|
|
94
|
+
self._exclude_noise = exclude_noise
|
|
95
|
+
self._exclude_tests = exclude_tests
|
|
96
|
+
self._ignore_learner = ignore_learner
|
|
97
|
+
# category -> count of files excluded during build (observability)
|
|
98
|
+
self.noise_excluded: dict[str, int] = {}
|
|
99
|
+
self.learner_excluded: int = 0
|
|
100
|
+
self._entries: dict[str, CorpusEntry] = {} # rel_path -> entry
|
|
101
|
+
self._built = False
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def entries(self) -> dict[str, CorpusEntry]:
|
|
105
|
+
if not self._built:
|
|
106
|
+
self._build()
|
|
107
|
+
return self._entries
|
|
108
|
+
|
|
109
|
+
def _build(self) -> None:
|
|
110
|
+
self._entries.clear()
|
|
111
|
+
self.noise_excluded.clear()
|
|
112
|
+
skip_dirs = self._skip_dirs
|
|
113
|
+
root = self.root
|
|
114
|
+
|
|
115
|
+
# Lazy import: keeps noise classification optional and avoids a
|
|
116
|
+
# brain -> security dependency at module load time.
|
|
117
|
+
classify = None
|
|
118
|
+
if self._exclude_noise or self._exclude_tests:
|
|
119
|
+
from patchi.core.security.noise_filter import (
|
|
120
|
+
classify as _classify,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
classify = _classify
|
|
124
|
+
|
|
125
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
126
|
+
dirnames[:] = [d for d in dirnames if d not in skip_dirs]
|
|
127
|
+
rel_dir = Path(dirpath).relative_to(root).as_posix()
|
|
128
|
+
if rel_dir == ".":
|
|
129
|
+
rel_dir = ""
|
|
130
|
+
|
|
131
|
+
for fname in filenames:
|
|
132
|
+
if fname in self._skip_files or is_minified_asset(fname):
|
|
133
|
+
continue
|
|
134
|
+
rel_path = f"{rel_dir}/{fname}" if rel_dir else fname
|
|
135
|
+
abs_path = Path(dirpath) / fname
|
|
136
|
+
|
|
137
|
+
if self._ignore_learner is not None:
|
|
138
|
+
try:
|
|
139
|
+
if self._ignore_learner.matches(rel_path) is not None:
|
|
140
|
+
self.learner_excluded += 1
|
|
141
|
+
continue
|
|
142
|
+
except Exception as _exc: # noqa: BLE001 — learner bugs never block scans
|
|
143
|
+
_log.debug('_build skipped: %s', _exc)
|
|
144
|
+
|
|
145
|
+
if classify is not None:
|
|
146
|
+
cat = classify(rel_path)
|
|
147
|
+
if cat is not None and (
|
|
148
|
+
(cat != "tests" and self._exclude_noise)
|
|
149
|
+
or (cat == "tests" and self._exclude_tests)
|
|
150
|
+
):
|
|
151
|
+
self.noise_excluded[cat] = self.noise_excluded.get(cat, 0) + 1
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
stat = abs_path.stat()
|
|
156
|
+
except OSError:
|
|
157
|
+
continue
|
|
158
|
+
if stat.st_size > self._max_size:
|
|
159
|
+
continue
|
|
160
|
+
if stat.st_size == 0:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
lang = detect_language(abs_path)
|
|
164
|
+
if lang == Lang.UNKNOWN:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
self._entries[rel_path] = CorpusEntry(
|
|
168
|
+
path=rel_path,
|
|
169
|
+
abs_path=abs_path,
|
|
170
|
+
language=lang,
|
|
171
|
+
size_bytes=stat.st_size,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self._built = True
|
|
175
|
+
|
|
176
|
+
def prune_with(self, learner) -> int:
|
|
177
|
+
"""Drop entries matching an IgnoreLearner (post-build composition pass).
|
|
178
|
+
|
|
179
|
+
Used because composition analysis needs the discovered file list
|
|
180
|
+
first; returns number of pruned entries.
|
|
181
|
+
"""
|
|
182
|
+
if not self._built or learner is None:
|
|
183
|
+
return 0
|
|
184
|
+
doomed = [rel for rel in self._entries if learner.matches(rel) is not None]
|
|
185
|
+
for rel in doomed:
|
|
186
|
+
del self._entries[rel]
|
|
187
|
+
self.learner_excluded += len(doomed)
|
|
188
|
+
return len(doomed)
|
|
189
|
+
|
|
190
|
+
# ── Query API ──────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
def files(self) -> list[CorpusEntry]:
|
|
193
|
+
return list(self.entries.values())
|
|
194
|
+
|
|
195
|
+
def by_language(self, lang: Lang) -> list[CorpusEntry]:
|
|
196
|
+
return [e for e in self.entries.values() if e.language == lang]
|
|
197
|
+
|
|
198
|
+
def by_ext(self, *extensions: str) -> list[CorpusEntry]:
|
|
199
|
+
exts = set(extensions)
|
|
200
|
+
return [e for e in self.entries.values() if e.abs_path.suffix.lower() in exts]
|
|
201
|
+
|
|
202
|
+
def by_glob(self, pattern: str) -> list[CorpusEntry]:
|
|
203
|
+
from fnmatch import fnmatch
|
|
204
|
+
|
|
205
|
+
return [e for e in self.entries.values() if fnmatch(e.path, pattern)]
|
|
206
|
+
|
|
207
|
+
def get(self, rel_path: str) -> CorpusEntry | None:
|
|
208
|
+
return self.entries.get(rel_path)
|
|
209
|
+
|
|
210
|
+
def read(self, rel_path: str) -> str | None:
|
|
211
|
+
entry = self.get(rel_path)
|
|
212
|
+
if entry is None:
|
|
213
|
+
return None
|
|
214
|
+
return entry.read()
|
|
215
|
+
|
|
216
|
+
def __len__(self) -> int:
|
|
217
|
+
return len(self.entries)
|
|
218
|
+
|
|
219
|
+
def __contains__(self, rel_path: str) -> bool:
|
|
220
|
+
return rel_path in self.entries
|