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.
Files changed (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. 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