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,710 @@
1
+ """
2
+ Shared syntax queries over tree-sitter — the replacement for per-agent regex.
3
+
4
+ Agents used to detect code constructs (setInterval calls, `var` declarations,
5
+ JSX text, `require()` …) with regular expressions, which misfire on comments,
6
+ strings, and formatting variants. These helpers parse once with tree-sitter
7
+ (error-tolerant: partial trees still yield captures) and answer structural
8
+ questions. All functions are fail-open: unparseable input returns empty
9
+ collections, never raises.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+
17
+ _log = logging.getLogger("patchi.brain.code_query")
18
+
19
+
20
+ @dataclass
21
+ class JsCall:
22
+ """A call site: property name, full dotted callee, line, args, span text."""
23
+
24
+ name: str
25
+ line: int
26
+ arg_kinds: list[str] = field(default_factory=list)
27
+ full: str = ""
28
+ text: str = ""
29
+
30
+
31
+ def _parser(lang: str):
32
+ try:
33
+ if lang == "tsx":
34
+ import tree_sitter_typescript as tsts
35
+ from tree_sitter import Language, Parser
36
+
37
+ return Parser(Language(tsts.language_tsx()))
38
+ from patchi.core.brain.languages import Lang, get_parser
39
+
40
+ return get_parser(Lang(lang))
41
+ except Exception as exc: # noqa: BLE001
42
+ _log.debug("code_query parser unavailable: %s", exc)
43
+ return None
44
+
45
+
46
+ def _query_text(lang_obj, pattern: str, node):
47
+ from tree_sitter import Query, QueryCursor
48
+
49
+ try:
50
+ return QueryCursor(Query(lang_obj, pattern)).captures(node)
51
+ except Exception as exc: # noqa: BLE001
52
+ _log.debug("code_query pattern failed %r: %s", pattern, exc)
53
+ return {}
54
+
55
+
56
+ def _lang_obj(lang: str):
57
+ from tree_sitter import Language
58
+
59
+ if lang == "typescript":
60
+ import tree_sitter_typescript as tsts
61
+
62
+ return Language(tsts.language_typescript())
63
+ if lang == "tsx":
64
+ import tree_sitter_typescript as tsts
65
+
66
+ return Language(tsts.language_tsx())
67
+ import tree_sitter_javascript as tsjs
68
+
69
+ return Language(tsjs.language())
70
+
71
+
72
+ def lang_for_file(filename: str) -> str:
73
+ """Tree-sitter language key for a path (tsx needs its own grammar)."""
74
+ name = filename.lower()
75
+ if name.endswith(".tsx"):
76
+ return "tsx"
77
+ if name.endswith((".ts", ".mts", ".cts")):
78
+ return "typescript"
79
+ return "javascript"
80
+
81
+
82
+ def _node_text(node) -> str:
83
+ t = node.text
84
+ if isinstance(t, bytes):
85
+ return t.decode("utf-8", errors="replace")
86
+ return str(t)
87
+
88
+
89
+ def _line(node) -> int:
90
+ return node.start_point[0] + 1
91
+
92
+
93
+ def parse_js(text: str, lang: str = "javascript"):
94
+ """Parse JS/JSX/TS/TSX source. Returns the tree or None (fail-open)."""
95
+ parser = _parser(lang)
96
+ if parser is None:
97
+ return None
98
+ try:
99
+ return parser.parse(text.encode("utf-8", errors="replace"))
100
+ except Exception as exc: # noqa: BLE001
101
+ _log.debug("code_query parse failed: %s", exc)
102
+ return None
103
+
104
+
105
+ def _callee_name(call_node) -> str:
106
+ fn = call_node.child_by_field_name("function")
107
+ if fn is None:
108
+ return ""
109
+ if fn.type == "identifier":
110
+ return _node_text(fn)
111
+ if fn.type == "member_expression":
112
+ prop = fn.child_by_field_name("property")
113
+ if prop is not None:
114
+ return _node_text(prop)
115
+ text = _node_text(fn)
116
+ return text.rsplit(".", 1)[-1] if "." in text else text
117
+ return ""
118
+
119
+
120
+ def js_calls(tree, lang: str = "javascript") -> list[JsCall]:
121
+ """Every call site with callee name, full dotted path, line, arg kinds."""
122
+ if tree is None:
123
+ return []
124
+ lang_obj = _lang_obj(lang)
125
+ out: list[JsCall] = []
126
+ for nodes in _query_text(lang_obj, "(call_expression) @c", tree.root_node).values():
127
+ for node in nodes:
128
+ args = node.child_by_field_name("arguments")
129
+ kinds = [c.type for c in args.named_children] if args is not None else []
130
+ fn = node.child_by_field_name("function")
131
+ full = _node_text(fn) if fn is not None else ""
132
+ out.append(
133
+ JsCall(
134
+ name=_callee_name(node),
135
+ line=_line(node),
136
+ arg_kinds=kinds,
137
+ full=full,
138
+ text=_node_text(node),
139
+ )
140
+ )
141
+ return out
142
+
143
+
144
+ def js_useeffect_without_cleanup(tree, lang: str = "javascript") -> list[int]:
145
+ """Lines of useEffect calls whose callback has no return statement."""
146
+ if tree is None:
147
+ return []
148
+ lang_obj = _lang_obj(lang)
149
+ out: list[int] = []
150
+ for nodes in _query_text(lang_obj, "(call_expression) @c", tree.root_node).values():
151
+ for node in nodes:
152
+ if _callee_name(node) != "useEffect":
153
+ continue
154
+ args = node.child_by_field_name("arguments")
155
+ if args is None:
156
+ continue
157
+ has_return = False
158
+ for arg in args.named_children:
159
+ if arg.type in ("arrow_function", "function_expression", "function"):
160
+ ret = _query_text(lang_obj, "(return_statement) @r", arg)
161
+ if any(ret.values()):
162
+ has_return = True
163
+ break
164
+ if not has_return:
165
+ out.append(_line(node))
166
+ return out
167
+
168
+
169
+ def js_call_names(tree, lang: str = "javascript") -> set[str]:
170
+ """Set of callee names called anywhere in the file."""
171
+ return {c.name for c in js_calls(tree, lang) if c.name}
172
+
173
+
174
+ def js_var_kinds(tree, lang: str = "javascript") -> list[tuple[str, int]]:
175
+ """(kind, line) for every var/let/const declaration."""
176
+ lang_obj = _lang_obj(lang)
177
+ out: list[tuple[str, int]] = []
178
+ if tree is None:
179
+ return []
180
+ found = _query_text(
181
+ lang_obj, "[(variable_declaration) (lexical_declaration)] @d", tree.root_node
182
+ )
183
+ for nodes in found.values():
184
+ for node in nodes:
185
+ text = _node_text(node).lstrip()
186
+ kind = text.split(None, 1)[0] if text else ""
187
+ if kind in ("var", "let", "const"):
188
+ out.append((kind, _line(node)))
189
+ return out
190
+
191
+
192
+ def js_has_await(tree, lang: str = "javascript") -> bool:
193
+ if tree is None:
194
+ return False
195
+ lang_obj = _lang_obj(lang)
196
+ return bool(_query_text(lang_obj, "(await_expression) @a", tree.root_node))
197
+
198
+
199
+ def js_new_names(tree, lang: str = "javascript") -> set[str]:
200
+ """Constructor names from `new X(...)` expressions."""
201
+ lang_obj = _lang_obj(lang)
202
+ out: set[str] = set()
203
+ found = _query_text(lang_obj, "(new_expression constructor: (_) @c)", tree.root_node)
204
+ for nodes in found.values():
205
+ for node in nodes:
206
+ out.add(_node_text(node))
207
+ return out
208
+
209
+
210
+ def js_jsx_texts(tree, lang: str = "javascript") -> list[tuple[str, int]]:
211
+ """Visible JSX text fragments with lines."""
212
+ lang_obj = _lang_obj(lang)
213
+ out: list[tuple[str, int]] = []
214
+ for nodes in _query_text(lang_obj, "(jsx_text) @t", tree.root_node).values():
215
+ for node in nodes:
216
+ text = _node_text(node).strip()
217
+ if text:
218
+ out.append((text, _line(node)))
219
+ return out
220
+
221
+
222
+ def js_class_heritages(tree, lang: str = "javascript") -> list[tuple[str, int]]:
223
+ """`extends ...` clause text with lines."""
224
+ lang_obj = _lang_obj(lang)
225
+ out: list[tuple[str, int]] = []
226
+ for nodes in _query_text(lang_obj, "(class_heritage) @h", tree.root_node).values():
227
+ for node in nodes:
228
+ out.append((_node_text(node), _line(node)))
229
+ return out
230
+
231
+
232
+ def js_identifiers(tree, lang: str = "javascript") -> set[str]:
233
+ """All identifier spellings (for $store-style prefix checks)."""
234
+ return {text for text, _line_no in js_identifier_lines(tree, lang)}
235
+
236
+
237
+ def js_property_names(tree, lang: str = "javascript") -> set[str]:
238
+ """Object/dict key spellings (property_identifier nodes)."""
239
+ if tree is None:
240
+ return set()
241
+ lang_obj = _lang_obj(lang)
242
+ out: set[str] = set()
243
+ for nodes in _query_text(lang_obj, "(property_identifier) @p", tree.root_node).values():
244
+ for node in nodes:
245
+ out.add(_node_text(node))
246
+ return out
247
+
248
+
249
+ def js_identifier_lines(tree, lang: str = "javascript") -> list[tuple[str, int]]:
250
+ """(identifier spelling, line) pairs."""
251
+ if tree is None:
252
+ return []
253
+ lang_obj = _lang_obj(lang)
254
+ out: list[tuple[str, int]] = []
255
+ for nodes in _query_text(lang_obj, "(identifier) @i", tree.root_node).values():
256
+ for node in nodes:
257
+ out.append((_node_text(node), _line(node)))
258
+ return out
259
+
260
+
261
+ def js_constructor_di_line(tree, lang: str = "javascript") -> int:
262
+ """Line of a constructor() with a private/parameter-property param, else 0."""
263
+ if tree is None:
264
+ return 0
265
+ lang_obj = _lang_obj(lang)
266
+ found = _query_text(lang_obj, "(method_definition) @m", tree.root_node)
267
+ for nodes in found.values():
268
+ for node in nodes:
269
+ name_node = node.child_by_field_name("name")
270
+ if name_node is None or _node_text(name_node) != "constructor":
271
+ continue
272
+ params = node.child_by_field_name("parameters")
273
+ if params is not None and "private" in _node_text(params):
274
+ return _line(node)
275
+ return 0
276
+
277
+
278
+ def js_string_literals(tree, lang: str = "javascript") -> list[tuple[str, int]]:
279
+ """String literal contents with lines (quotes stripped)."""
280
+ lang_obj = _lang_obj(lang)
281
+ out: list[tuple[str, int]] = []
282
+ for nodes in _query_text(lang_obj, "(string) @s", tree.root_node).values():
283
+ for node in nodes:
284
+ text = _node_text(node)
285
+ if len(text) >= 2 and text[0] == text[-1] and text[0] in "\"'`":
286
+ text = text[1:-1]
287
+ out.append((text, _line(node)))
288
+ return out
289
+
290
+
291
+ def js_has_try(tree, lang: str = "javascript") -> bool:
292
+ if tree is None:
293
+ return False
294
+ lang_obj = _lang_obj(lang)
295
+ return bool(_query_text(lang_obj, "(try_statement) @t", tree.root_node))
296
+
297
+
298
+ def _catch_in_subtree(lang_obj, node) -> bool:
299
+ sub = _query_text(lang_obj, "(call_expression) @c", node)
300
+ return any(_callee_name(n) == "catch" for ns in sub.values() for n in ns)
301
+
302
+
303
+ def _assigned_name(stmt) -> str:
304
+ if stmt.type == "variable_declarator":
305
+ name_node = stmt.child_by_field_name("name")
306
+ if name_node is not None:
307
+ return _node_text(name_node)
308
+ return ""
309
+
310
+
311
+ def js_new_without_catch(tree, names=("Promise",), lang: str = "javascript") -> list[int]:
312
+ """Lines of `new X(...)` with no `.catch` — same statement or later `v.catch`."""
313
+ if tree is None:
314
+ return []
315
+ lang_obj = _lang_obj(lang)
316
+ out: list[int] = []
317
+ for nodes in _query_text(lang_obj, "(new_expression) @n", tree.root_node).values():
318
+ for node in nodes:
319
+ ctor = node.child_by_field_name("constructor")
320
+ cname = _node_text(ctor) if ctor is not None else ""
321
+ if cname not in names:
322
+ continue
323
+ stmt = node
324
+ var_name = ""
325
+ while stmt.parent is not None and stmt.type not in (
326
+ "expression_statement",
327
+ "variable_declaration",
328
+ "lexical_declaration",
329
+ "return_statement",
330
+ ):
331
+ if stmt.type == "variable_declarator" and not var_name:
332
+ var_name = _assigned_name(stmt)
333
+ stmt = stmt.parent
334
+ if _catch_in_subtree(lang_obj, stmt):
335
+ continue
336
+ if var_name:
337
+ parent = stmt.parent
338
+ if parent is not None:
339
+ kids = parent.named_children
340
+ try:
341
+ idx = kids.index(stmt)
342
+ except ValueError:
343
+ idx = -1
344
+ handled = False
345
+ for sib in kids[idx + 1 :]:
346
+ sib_calls = _query_text(lang_obj, "(call_expression) @c", sib)
347
+ for ns in sib_calls.values():
348
+ for n in ns:
349
+ fn = n.child_by_field_name("function")
350
+ if (
351
+ fn is not None
352
+ and fn.type == "member_expression"
353
+ and _node_text(fn).startswith(var_name + ".")
354
+ and _callee_name(n) == "catch"
355
+ ):
356
+ handled = True
357
+ break
358
+ if handled:
359
+ break
360
+ if handled:
361
+ break
362
+ if handled:
363
+ continue
364
+ out.append(_line(node))
365
+ return out
366
+
367
+
368
+ def js_jsx_elements(tree, lang: str = "javascript") -> list[tuple[str, int]]:
369
+ """(tag, line) for every JSX opening element."""
370
+ if tree is None:
371
+ return []
372
+ lang_obj = _lang_obj(lang)
373
+ out: list[tuple[str, int]] = []
374
+ found = _query_text(lang_obj, "(jsx_opening_element) @o", tree.root_node)
375
+ for nodes in found.values():
376
+ for node in nodes:
377
+ for child in node.named_children:
378
+ if child.type == "identifier":
379
+ out.append((_node_text(child), _line(node)))
380
+ break
381
+ return out
382
+
383
+
384
+ def js_jsx_attributes(tree, lang: str = "javascript") -> list[tuple[str, str, int]]:
385
+ """(tag, attribute_name, line) for every JSX attribute."""
386
+ if tree is None:
387
+ return []
388
+ lang_obj = _lang_obj(lang)
389
+ out: list[tuple[str, str, int]] = []
390
+ found = _query_text(lang_obj, "(jsx_opening_element) @o", tree.root_node)
391
+ for nodes in found.values():
392
+ for node in nodes:
393
+ tag = ""
394
+ for child in node.named_children:
395
+ if child.type == "identifier":
396
+ tag = _node_text(child)
397
+ break
398
+ for child in node.named_children:
399
+ if child.type == "jsx_attribute":
400
+ name_node = child.child_by_field_name("name")
401
+ if name_node is None and child.named_children:
402
+ name_node = child.named_children[0]
403
+ if name_node is not None:
404
+ out.append((tag, _node_text(name_node), _line(child)))
405
+ return out
406
+
407
+
408
+ def extract_script_blocks(text: str) -> list[str]:
409
+ """Raw JS from <script> blocks in SFC/HTML (for .vue/.svelte parsing)."""
410
+ from html.parser import HTMLParser
411
+
412
+ out: list[str] = []
413
+ current: list[str] | None = None
414
+
415
+ class _P(HTMLParser):
416
+ def handle_starttag(self, tag, attrs):
417
+ nonlocal current
418
+ if tag == "script":
419
+ current = []
420
+
421
+ def handle_endtag(self, tag):
422
+ nonlocal current
423
+ if tag == "script" and current is not None:
424
+ out.append("".join(current))
425
+ current = None
426
+
427
+ def handle_data(self, data):
428
+ if current is not None:
429
+ current.append(data)
430
+
431
+ try:
432
+ _P().feed(text)
433
+ except Exception as exc: # noqa: BLE001
434
+ _log.debug("script extract failed: %s", exc)
435
+ return out
436
+
437
+
438
+ def vue_template_texts(text: str) -> list[tuple[str, int]]:
439
+ """Visible text nodes from Vue/SFC/HTML markup via html.parser."""
440
+ from html.parser import HTMLParser
441
+
442
+ out: list[tuple[str, int]] = []
443
+
444
+ class _P(HTMLParser):
445
+ def handle_data(self, data):
446
+ rest, chunks = data, []
447
+ while "{{" in rest and "}}" in rest:
448
+ before, _, rest = rest.partition("{{")
449
+ chunks.append(before)
450
+ _, _, rest = rest.partition("}}")
451
+ chunks.append(rest)
452
+ stripped = " ".join(chunks).strip()
453
+ if stripped:
454
+ line, _ = self.getpos()
455
+ out.append((stripped, line))
456
+
457
+ try:
458
+ _P().feed(text)
459
+ except Exception as exc: # noqa: BLE001
460
+ _log.debug("template text parse failed: %s", exc)
461
+ return out
462
+
463
+
464
+ def string_literals(text: str, lang) -> list[tuple[str, int]]:
465
+ """(unquoted value, 1-based line) for string/symbol literals, any grammar.
466
+
467
+ Tries common string node types so one helper serves every installed
468
+ tree-sitter grammar. Fail-open: [] when the grammar is missing.
469
+ """
470
+ from tree_sitter import Query, QueryCursor
471
+
472
+ try:
473
+ from patchi.core.brain.languages import get_parser
474
+
475
+ parser = get_parser(lang)
476
+ except Exception:
477
+ return []
478
+ if parser is None:
479
+ return []
480
+ try:
481
+ tree = parser.parse(text.encode("utf-8", errors="replace"))
482
+ except Exception:
483
+ return []
484
+ try:
485
+ language = tree.language
486
+ except Exception:
487
+ return []
488
+ seen: set[tuple[str, int]] = set()
489
+ out: list[tuple[str, int]] = []
490
+ patterns = (
491
+ "(string) @s",
492
+ "(string_literal) @s",
493
+ "(interpreted_string_literal) @s",
494
+ "(raw_string_literal) @s",
495
+ "(encapsed_string) @s",
496
+ "(symbol) @s",
497
+ )
498
+ for pattern in patterns:
499
+ try:
500
+ found = QueryCursor(Query(language, pattern)).captures(tree.root_node)
501
+ except Exception:
502
+ continue
503
+ for nodes in found.values():
504
+ for node in nodes:
505
+ raw = _node_text(node)
506
+ val = raw.strip()
507
+ if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'`":
508
+ val = val[1:-1]
509
+ val = val.lstrip(":")
510
+ if val:
511
+ key = (val, _line(node))
512
+ if key not in seen:
513
+ seen.add(key)
514
+ out.append(key)
515
+ return out
516
+
517
+
518
+ def js_string_pairs(tree, lang: str = "javascript") -> list[tuple[str, str, int]]:
519
+ """(key, value, line) for object properties with string values."""
520
+ if tree is None:
521
+ return []
522
+ lang_obj = _lang_obj(lang)
523
+ out: list[tuple[str, str, int]] = []
524
+ for nodes in _query_text(lang_obj, "(pair) @p", tree.root_node).values():
525
+ for node in nodes:
526
+ key_node = node.child_by_field_name("key")
527
+ val_node = node.child_by_field_name("value")
528
+ if key_node is None or val_node is None:
529
+ continue
530
+ key = _node_text(key_node).strip("\"'`")
531
+ val = _node_text(val_node).strip()
532
+ if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'`":
533
+ val = val[1:-1]
534
+ out.append((key, val, _line(node)))
535
+ return out
536
+
537
+
538
+ def member_paths(text: str, lang) -> list[tuple[str, int]]:
539
+ """(dotted member-access text, line) for attribute/subscript/member nodes.
540
+
541
+ `lang` is a languages.Lang enum member. Tries member-ish node types across
542
+ grammars (member_expression, attribute, subscript_expression, ...),
543
+ skipping types the grammar lacks. Fail-open: [] when unparseable.
544
+ """
545
+ from tree_sitter import Query, QueryCursor
546
+
547
+ try:
548
+ from patchi.core.brain.languages import get_parser
549
+
550
+ parser = get_parser(lang)
551
+ except Exception:
552
+ return []
553
+ if parser is None:
554
+ return []
555
+ try:
556
+ tree = parser.parse(text.encode("utf-8", errors="replace"))
557
+ language = tree.language
558
+ except Exception:
559
+ return []
560
+ seen: set[tuple[str, int]] = set()
561
+ out: list[tuple[str, int]] = []
562
+ for node_type in (
563
+ "member_expression",
564
+ "attribute",
565
+ "subscript_expression",
566
+ "member_access_expression",
567
+ "scoped_identifier",
568
+ "field_expression",
569
+ ):
570
+ try:
571
+ found = QueryCursor(Query(language, f"({node_type}) @m")).captures(
572
+ tree.root_node
573
+ )
574
+ except Exception:
575
+ continue
576
+ for nodes in found.values():
577
+ for node in nodes:
578
+ text_val = _node_text(node)
579
+ # Skip bare identifiers and call-covered nodes; keep dotted paths
580
+ if "." not in text_val and "[" not in text_val:
581
+ continue
582
+ key = (text_val, _line(node))
583
+ if key not in seen:
584
+ seen.add(key)
585
+ out.append(key)
586
+ return out
587
+
588
+
589
+ def calls_with_dynamic_arg(text: str, lang, names: set[str]) -> list[int]:
590
+ """Lines of calls to `names` whose arguments build strings dynamically.
591
+
592
+ Python: +/% BinOp, f-strings, .format() in call args (stdlib ast).
593
+ JS/TS: binary_expression / template_string in call args (tree-sitter).
594
+ `lang` is a languages.Lang enum member. Fail-open: [].
595
+ """
596
+ import ast as _ast
597
+
598
+ try:
599
+ from patchi.core.brain.languages import Lang
600
+ except Exception:
601
+ return []
602
+ wanted = {n.split(".")[-1] for n in names} | set(names)
603
+ if lang == Lang.PYTHON:
604
+ try:
605
+ tree = _ast.parse(text)
606
+ except SyntaxError:
607
+ return []
608
+ out: list[int] = []
609
+ for node in _ast.walk(tree):
610
+ if not isinstance(node, _ast.Call):
611
+ continue
612
+ func = node.func
613
+ if isinstance(func, _ast.Name):
614
+ dotted = func.id
615
+ elif isinstance(func, _ast.Attribute):
616
+ parts, cur = [], func
617
+ while isinstance(cur, _ast.Attribute):
618
+ parts.append(cur.attr)
619
+ cur = cur.value
620
+ if isinstance(cur, _ast.Name):
621
+ parts.append(cur.id)
622
+ dotted = ".".join(reversed(parts))
623
+ else:
624
+ continue
625
+ if dotted not in names and dotted.split(".")[-1] not in wanted:
626
+ continue
627
+ dynamic = False
628
+ for arg in list(node.args) + [kw.value for kw in node.keywords]:
629
+ for sub in _ast.walk(arg):
630
+ if isinstance(sub, (_ast.BinOp, _ast.JoinedStr)):
631
+ dynamic = True
632
+ break
633
+ if isinstance(sub, _ast.Call):
634
+ f = sub.func
635
+ fn = f.id if isinstance(f, _ast.Name) else getattr(f, "attr", "")
636
+ if fn == "format":
637
+ dynamic = True
638
+ break
639
+ if dynamic:
640
+ break
641
+ if dynamic:
642
+ out.append(getattr(node, "lineno", 0))
643
+ return sorted(set(out))
644
+ # tree-sitter languages: dynamic = binary/template args in the call span
645
+ try:
646
+ from patchi.core.brain.languages import get_parser
647
+
648
+ parser = get_parser(lang)
649
+ except Exception:
650
+ return []
651
+ if parser is None:
652
+ return []
653
+ try:
654
+ parsed = parser.parse(text.encode("utf-8", errors="replace"))
655
+ language = parsed.language
656
+ except Exception:
657
+ return []
658
+ from tree_sitter import Query, QueryCursor
659
+
660
+ out = []
661
+ try:
662
+ found = QueryCursor(
663
+ Query(language, "(call_expression) @c")
664
+ ).captures(parsed.root_node)
665
+ except Exception:
666
+ return []
667
+ for nodes in found.values():
668
+ for node in nodes:
669
+ fn = node.child_by_field_name("function")
670
+ dotted = _node_text(fn) if fn is not None else ""
671
+ if dotted not in names and dotted.split(".")[-1] not in wanted:
672
+ continue
673
+ args = node.child_by_field_name("arguments")
674
+ if args is None:
675
+ continue
676
+ try:
677
+ dyn = QueryCursor(
678
+ Query(
679
+ language,
680
+ "[(binary_expression) (template_string) (template_substitution)] @d",
681
+ )
682
+ ).captures(args)
683
+ except Exception:
684
+ continue
685
+ if any(dyn.values()):
686
+ out.append(_line(node))
687
+ return sorted(set(out))
688
+
689
+
690
+ def vue_template_attrs(text: str) -> list[tuple[str, str, int]]:
691
+ """(tag, attr_name, line) from Vue/SFC template markup via html.parser.
692
+
693
+ tree-sitter-javascript cannot parse HTML templates; the stdlib HTML parser
694
+ reads real tag structure instead of regexing angle brackets.
695
+ """
696
+ from html.parser import HTMLParser
697
+
698
+ out: list[tuple[str, str, int]] = []
699
+
700
+ class _P(HTMLParser):
701
+ def handle_starttag(self, tag, attrs):
702
+ line, _ = self.getpos()
703
+ for name, _value in attrs:
704
+ out.append((tag, name, line))
705
+
706
+ try:
707
+ _P().feed(text)
708
+ except Exception as exc: # noqa: BLE001
709
+ _log.debug("vue template parse failed: %s", exc)
710
+ return out