bash-classify 0.3.0__tar.gz → 0.4.0__tar.gz

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 (195) hide show
  1. {bash_classify-0.3.0 → bash_classify-0.4.0}/.claude-plugin/marketplace.json +1 -1
  2. {bash_classify-0.3.0 → bash_classify-0.4.0}/PKG-INFO +1 -1
  3. {bash_classify-0.3.0 → bash_classify-0.4.0}/SPEC.md +43 -0
  4. {bash_classify-0.3.0 → bash_classify-0.4.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
  5. {bash_classify-0.3.0 → bash_classify-0.4.0}/docs/classification-guidance.md +44 -1
  6. {bash_classify-0.3.0 → bash_classify-0.4.0}/schemas/command.schema.json +11 -0
  7. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gradle.yaml +34 -0
  8. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gradlew.yaml +34 -0
  9. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mvn.yaml +30 -0
  10. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mvnw.yaml +30 -0
  11. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/poetry.yaml +2 -0
  12. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uv.yaml +2 -0
  13. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/database.py +10 -1
  14. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/matcher.py +65 -1
  15. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/models.py +8 -0
  16. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_matcher.py +75 -1
  17. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_models.py +7 -1
  18. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_real_world.py +32 -0
  19. {bash_classify-0.3.0 → bash_classify-0.4.0}/.claude/rules/command-database.md +0 -0
  20. {bash_classify-0.3.0 → bash_classify-0.4.0}/.editorconfig +0 -0
  21. {bash_classify-0.3.0 → bash_classify-0.4.0}/.github/workflows/ci.yml +0 -0
  22. {bash_classify-0.3.0 → bash_classify-0.4.0}/.github/workflows/publish.yml +0 -0
  23. {bash_classify-0.3.0 → bash_classify-0.4.0}/.gitignore +0 -0
  24. {bash_classify-0.3.0 → bash_classify-0.4.0}/.python-version +0 -0
  25. {bash_classify-0.3.0 → bash_classify-0.4.0}/CLAUDE.md +0 -0
  26. {bash_classify-0.3.0 → bash_classify-0.4.0}/LICENSE +0 -0
  27. {bash_classify-0.3.0 → bash_classify-0.4.0}/README.md +0 -0
  28. {bash_classify-0.3.0 → bash_classify-0.4.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh +0 -0
  29. {bash_classify-0.3.0 → bash_classify-0.4.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
  30. {bash_classify-0.3.0 → bash_classify-0.4.0}/pyproject.toml +0 -0
  31. {bash_classify-0.3.0 → bash_classify-0.4.0}/ruff.toml +0 -0
  32. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/__init__.py +0 -0
  33. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/__main__.py +0 -0
  34. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/classifier.py +0 -0
  35. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/cli.py +0 -0
  36. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
  37. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/apt.yaml +0 -0
  38. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/awk.yaml +0 -0
  39. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/basename.yaml +0 -0
  40. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
  41. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/bash.yaml +0 -0
  42. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/black.yaml +0 -0
  43. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/brew.yaml +0 -0
  44. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/builtin.yaml +0 -0
  45. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cargo.yaml +0 -0
  46. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cat.yaml +0 -0
  47. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/chgrp.yaml +0 -0
  48. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/chmod.yaml +0 -0
  49. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/chown.yaml +0 -0
  50. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/column.yaml +0 -0
  51. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/comm.yaml +0 -0
  52. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/command.yaml +0 -0
  53. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cp.yaml +0 -0
  54. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/curl.yaml +0 -0
  55. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/cut.yaml +0 -0
  56. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/date.yaml +0 -0
  57. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/df.yaml +0 -0
  58. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/diff.yaml +0 -0
  59. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/dig.yaml +0 -0
  60. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/dirname.yaml +0 -0
  61. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/docker.yaml +0 -0
  62. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/du.yaml +0 -0
  63. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/echo.yaml +0 -0
  64. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/env.yaml +0 -0
  65. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/eslint.yaml +0 -0
  66. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/expand.yaml +0 -0
  67. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/false.yaml +0 -0
  68. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/file.yaml +0 -0
  69. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/find.yaml +0 -0
  70. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/fmt.yaml +0 -0
  71. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/fold.yaml +0 -0
  72. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/free.yaml +0 -0
  73. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gawk.yaml +0 -0
  74. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gcloud.yaml +0 -0
  75. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gh.yaml +0 -0
  76. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/git.yaml +0 -0
  77. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
  78. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/glab.yaml +0 -0
  79. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/go.yaml +0 -0
  80. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/grep.yaml +0 -0
  81. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/groups.yaml +0 -0
  82. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gunzip.yaml +0 -0
  83. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/gzip.yaml +0 -0
  84. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/hadolint.yaml +0 -0
  85. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/head.yaml +0 -0
  86. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/helm.yaml +0 -0
  87. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/hostname.yaml +0 -0
  88. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/id.yaml +0 -0
  89. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/install.yaml +0 -0
  90. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/isort.yaml +0 -0
  91. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/java.yaml +0 -0
  92. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/javac.yaml +0 -0
  93. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/join.yaml +0 -0
  94. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/journalctl.yaml +0 -0
  95. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/jq.yaml +0 -0
  96. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/kill.yaml +0 -0
  97. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/killall.yaml +0 -0
  98. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/kubectl.yaml +0 -0
  99. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/less.yaml +0 -0
  100. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ln.yaml +0 -0
  101. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ls.yaml +0 -0
  102. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/make.yaml +0 -0
  103. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/md5sum.yaml +0 -0
  104. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mkdir.yaml +0 -0
  105. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/more.yaml +0 -0
  106. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mv.yaml +0 -0
  107. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/mypy.yaml +0 -0
  108. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/netstat.yaml +0 -0
  109. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nice.yaml +0 -0
  110. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nl.yaml +0 -0
  111. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/node.yaml +0 -0
  112. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nohup.yaml +0 -0
  113. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/npm.yaml +0 -0
  114. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/npx.yaml +0 -0
  115. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/nslookup.yaml +0 -0
  116. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/paste.yaml +0 -0
  117. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ping.yaml +0 -0
  118. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pip.yaml +0 -0
  119. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pip3.yaml +0 -0
  120. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pipx.yaml +0 -0
  121. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pkill.yaml +0 -0
  122. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/prettier.yaml +0 -0
  123. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/printenv.yaml +0 -0
  124. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/printf.yaml +0 -0
  125. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ps.yaml +0 -0
  126. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pwd.yaml +0 -0
  127. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/pytest.yaml +0 -0
  128. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/python.yaml +0 -0
  129. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/python3.yaml +0 -0
  130. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/read.yaml +0 -0
  131. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/readlink.yaml +0 -0
  132. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/realpath.yaml +0 -0
  133. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rev.yaml +0 -0
  134. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rg.yaml +0 -0
  135. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rm.yaml +0 -0
  136. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/rsync.yaml +0 -0
  137. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ruff.yaml +0 -0
  138. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/scp.yaml +0 -0
  139. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sed.yaml +0 -0
  140. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/seq.yaml +0 -0
  141. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sh.yaml +0 -0
  142. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
  143. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
  144. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
  145. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sort.yaml +0 -0
  146. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ss.yaml +0 -0
  147. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/ssh.yaml +0 -0
  148. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/stat.yaml +0 -0
  149. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/strace.yaml +0 -0
  150. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/sudo.yaml +0 -0
  151. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/systemctl.yaml +0 -0
  152. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tac.yaml +0 -0
  153. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tail.yaml +0 -0
  154. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tar.yaml +0 -0
  155. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tee.yaml +0 -0
  156. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/terraform.yaml +0 -0
  157. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/test.yaml +0 -0
  158. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/time.yaml +0 -0
  159. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/timeout.yaml +0 -0
  160. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/top.yaml +0 -0
  161. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/touch.yaml +0 -0
  162. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tr.yaml +0 -0
  163. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/traceroute.yaml +0 -0
  164. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/true.yaml +0 -0
  165. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/tsc.yaml +0 -0
  166. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/type.yaml +0 -0
  167. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uname.yaml +0 -0
  168. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/unexpand.yaml +0 -0
  169. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uniq.yaml +0 -0
  170. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/unzip.yaml +0 -0
  171. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/uptime.yaml +0 -0
  172. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/wc.yaml +0 -0
  173. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/wget.yaml +0 -0
  174. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/whereis.yaml +0 -0
  175. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/which.yaml +0 -0
  176. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/whoami.yaml +0 -0
  177. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/xargs.yaml +0 -0
  178. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/xmllint.yaml +0 -0
  179. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/yarn.yaml +0 -0
  180. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/yes.yaml +0 -0
  181. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/yq.yaml +0 -0
  182. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/zip.yaml +0 -0
  183. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/commands/zsh.yaml +0 -0
  184. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/parser.py +0 -0
  185. {bash_classify-0.3.0 → bash_classify-0.4.0}/src/bash_classify/py.typed +0 -0
  186. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/__init__.py +0 -0
  187. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/conftest.py +0 -0
  188. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_classifier.py +0 -0
  189. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_cli.py +0 -0
  190. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_cli_serialization.py +0 -0
  191. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_database.py +0 -0
  192. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_parser.py +0 -0
  193. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_schema.py +0 -0
  194. {bash_classify-0.3.0 → bash_classify-0.4.0}/tests/test_smoke.py +0 -0
  195. {bash_classify-0.3.0 → bash_classify-0.4.0}/uv.lock +0 -0
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "bash-classify-hook",
10
10
  "description": "Auto-allows low-risk bash commands in Claude Code using bash-classify",
11
- "version": "0.3.0",
11
+ "version": "0.4.0",
12
12
  "author": {
13
13
  "name": "Filip Procházka",
14
14
  "url": "https://github.com/fprochazka"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bash-classify
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Classify bash commands as READONLY, WRITE, DANGEROUS, or UNKNOWN
5
5
  Project-URL: Homepage, https://github.com/fprochazka/bash-classify
6
6
  Project-URL: Repository, https://github.com/fprochazka/bash-classify
@@ -345,6 +345,48 @@ The database defines which options take values, so the parser knows whether `-n
345
345
 
346
346
  If the matched subcommand has `strict: true` (default), any unrecognized option makes the command `UNKNOWN`. If `strict: false`, unrecognized options are ignored (useful for commands with too many harmless flags to enumerate).
347
347
 
348
+ ### Subcommand matching modes
349
+
350
+ The `subcommand_mode` field controls how positional arguments are matched against the subcommand dictionary. There are two modes:
351
+
352
+ #### `hierarchical` (default)
353
+
354
+ The default mode. Subcommands are matched as a greedy chain through nested subcommand dictionaries. Each matched subcommand narrows the context to its own subcommand dict.
355
+
356
+ Example: `kubectl rollout status` matches `kubectl` → `rollout` → `status`, walking the tree.
357
+
358
+ #### `match_all`
359
+
360
+ Each positional argument is matched independently against the **same** top-level subcommand dictionary. This is designed for build tools like Maven and Gradle where multiple goals/tasks appear as positional arguments in any order.
361
+
362
+ Example: `mvn clean install` matches both `clean` and `install` independently against `mvn`'s subcommand dict.
363
+
364
+ **Classification/risk aggregation in `match_all` mode:**
365
+
366
+ - The final classification is the **maximum severity** across all matched goals.
367
+ - The final risk is the **maximum** across all matched goals' risks.
368
+ - If any positional argument is **unrecognized** (not in the subcommand dict), the command's base classification and risk are included in the aggregation.
369
+ - If **no goals match** (no positional args or all unrecognized), the command's base classification and risk apply.
370
+ - Option overrides (e.g. `--dry-run`) take precedence over goal aggregation — they replace the classification as usual.
371
+ - The `strict` mode only applies to **options** (flags starting with `-`), not to unrecognized positional arguments. Unrecognized goals do not cause UNKNOWN classification.
372
+
373
+ ```
374
+ argv: ["mvn", "-B", "clean", "deploy"]
375
+
376
+ Step 1: binary lookup → found "mvn" in database (subcommand_mode: match_all)
377
+ Step 2: strip global options → none
378
+ Step 3: subcommand matching (match_all mode)
379
+ - "-B" starts with "-" → passed to option classification
380
+ - "clean" found in subcommands → LOCAL_EFFECTS, risk LOW
381
+ - "deploy" found in subcommands → EXTERNAL_EFFECTS, risk MEDIUM (default)
382
+ - aggregate: classification = EXTERNAL_EFFECTS, risk = MEDIUM
383
+ Step 4: option classification
384
+ - "-B" is a known option (no override)
385
+ Step 5: classification
386
+ - aggregate classification: EXTERNAL_EFFECTS
387
+ - no option overrides → final: EXTERNAL_EFFECTS, risk: MEDIUM
388
+ ```
389
+
348
390
  ## Command Database Format
349
391
 
350
392
  YAML files, one per command or command family. Loaded from a `commands/` directory.
@@ -807,6 +849,7 @@ The `delegates_to` field defines how a command (or option like `find -exec`) han
807
849
  | `subcommands` | `map` | Nested subcommand definitions (recursive structure) |
808
850
  | `options` | `map` | Options that affect classification of this command/subcommand |
809
851
  | `strict` | `bool` | If `true` (default), unrecognized options yield `UNKNOWN` |
852
+ | `subcommand_mode` | `enum` | How subcommands are matched: `hierarchical` (default, chained tree walk) or `match_all` (each positional arg matched independently) |
810
853
  | `delegates_to` | `object` | How this command delegates execution to an inner command (see delegation modes) |
811
854
 
812
855
  #### Option level
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bash-classify-hook",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Auto-allows low-risk bash commands in Claude Code using bash-classify",
5
5
  "author": {
6
6
  "name": "Filip Prochazka"
@@ -410,7 +410,50 @@ subcommands:
410
410
  --dry-run: {takes_value: true, overrides: READONLY, risk: LOW}
411
411
  ```
412
412
 
413
- ## 9. Common Patterns
413
+ ## 9. Subcommand Matching Modes
414
+
415
+ ### `subcommand_mode: hierarchical` (default)
416
+
417
+ The default mode. Subcommands form a tree, and matching walks the tree greedily. This is correct for most CLI tools where subcommands are hierarchical (e.g. `kubectl rollout status`, `git stash list`).
418
+
419
+ ### `subcommand_mode: match_all`
420
+
421
+ Use for build tools that accept multiple goals or tasks as positional arguments in any order. Each positional argument is matched independently against the same subcommand dictionary.
422
+
423
+ ```yaml
424
+ # Maven accepts multiple lifecycle phases: mvn clean install
425
+ command: mvn
426
+ classification: LOCAL_EFFECTS
427
+ strict: false
428
+ subcommand_mode: match_all
429
+ subcommands:
430
+ clean:
431
+ classification: LOCAL_EFFECTS
432
+ risk: LOW
433
+ compile:
434
+ classification: LOCAL_EFFECTS
435
+ risk: LOW
436
+ install:
437
+ classification: LOCAL_EFFECTS
438
+ risk: LOW
439
+ deploy:
440
+ classification: EXTERNAL_EFFECTS
441
+ ```
442
+
443
+ **When to use `match_all`:**
444
+
445
+ - Build tools that accept multiple goals/tasks (Maven, Gradle)
446
+ - Commands where positional args are independent operations, not hierarchical nesting
447
+
448
+ **How classification works:**
449
+
450
+ - The final classification is the maximum severity across all matched goals
451
+ - `mvn clean install` → both LOCAL_EFFECTS → final: LOCAL_EFFECTS
452
+ - `mvn clean deploy` → LOCAL_EFFECTS + EXTERNAL_EFFECTS → final: EXTERNAL_EFFECTS
453
+ - Unrecognized goals (e.g. custom Maven plugin goals like `some-plugin:goal`) fall back to the command's base classification and risk
454
+ - Option overrides (e.g. `--dry-run`) still take precedence over goal aggregation
455
+
456
+ ## 10. Common Patterns
414
457
 
415
458
  | Pattern | Example | Classification |
416
459
  |---------|---------|---------------|
@@ -38,6 +38,9 @@
38
38
  "$ref": "#/$defs/SubcommandsMap",
39
39
  "description": "Nested subcommand definitions"
40
40
  },
41
+ "subcommand_mode": {
42
+ "$ref": "#/$defs/SubcommandMode"
43
+ },
41
44
  "delegates_to": {
42
45
  "$ref": "#/$defs/DelegationConfig"
43
46
  }
@@ -53,6 +56,11 @@
53
56
  "enum": ["LOW", "MEDIUM", "HIGH"],
54
57
  "description": "Risk level for a command or option override"
55
58
  },
59
+ "SubcommandMode": {
60
+ "type": "string",
61
+ "enum": ["hierarchical", "match_all"],
62
+ "description": "How subcommands are matched: hierarchical (chained tree walk, default) or match_all (each positional arg matched independently)"
63
+ },
56
64
  "DelegationConfig": {
57
65
  "type": "object",
58
66
  "required": ["mode"],
@@ -139,6 +147,9 @@
139
147
  "type": "boolean",
140
148
  "description": "If true (default), unrecognized options yield UNKNOWN"
141
149
  },
150
+ "subcommand_mode": {
151
+ "$ref": "#/$defs/SubcommandMode"
152
+ },
142
153
  "options": {
143
154
  "$ref": "#/$defs/OptionsMap"
144
155
  },
@@ -3,6 +3,40 @@ command: gradle
3
3
  description: "Gradle build tool"
4
4
  classification: LOCAL_EFFECTS
5
5
  strict: false
6
+ subcommand_mode: match_all
7
+ subcommands:
8
+ clean:
9
+ classification: LOCAL_EFFECTS
10
+ risk: LOW
11
+ build:
12
+ classification: LOCAL_EFFECTS
13
+ risk: LOW
14
+ test:
15
+ classification: LOCAL_EFFECTS
16
+ risk: LOW
17
+ assemble:
18
+ classification: LOCAL_EFFECTS
19
+ risk: LOW
20
+ check:
21
+ classification: LOCAL_EFFECTS
22
+ risk: LOW
23
+ jar:
24
+ classification: LOCAL_EFFECTS
25
+ risk: LOW
26
+ classes:
27
+ classification: LOCAL_EFFECTS
28
+ risk: LOW
29
+ compileJava:
30
+ classification: LOCAL_EFFECTS
31
+ risk: LOW
32
+ compileKotlin:
33
+ classification: LOCAL_EFFECTS
34
+ risk: LOW
35
+ publishToMavenLocal:
36
+ classification: LOCAL_EFFECTS
37
+ risk: LOW
38
+ publish:
39
+ classification: EXTERNAL_EFFECTS
6
40
  global_options:
7
41
  --help: {overrides: READONLY}
8
42
  -h: {overrides: READONLY}
@@ -3,6 +3,40 @@ command: gradlew
3
3
  description: "Gradle wrapper"
4
4
  classification: LOCAL_EFFECTS
5
5
  strict: false
6
+ subcommand_mode: match_all
7
+ subcommands:
8
+ clean:
9
+ classification: LOCAL_EFFECTS
10
+ risk: LOW
11
+ build:
12
+ classification: LOCAL_EFFECTS
13
+ risk: LOW
14
+ test:
15
+ classification: LOCAL_EFFECTS
16
+ risk: LOW
17
+ assemble:
18
+ classification: LOCAL_EFFECTS
19
+ risk: LOW
20
+ check:
21
+ classification: LOCAL_EFFECTS
22
+ risk: LOW
23
+ jar:
24
+ classification: LOCAL_EFFECTS
25
+ risk: LOW
26
+ classes:
27
+ classification: LOCAL_EFFECTS
28
+ risk: LOW
29
+ compileJava:
30
+ classification: LOCAL_EFFECTS
31
+ risk: LOW
32
+ compileKotlin:
33
+ classification: LOCAL_EFFECTS
34
+ risk: LOW
35
+ publishToMavenLocal:
36
+ classification: LOCAL_EFFECTS
37
+ risk: LOW
38
+ publish:
39
+ classification: EXTERNAL_EFFECTS
6
40
  global_options:
7
41
  --help: {overrides: READONLY}
8
42
  -h: {overrides: READONLY}
@@ -3,6 +3,36 @@ command: mvn
3
3
  description: "Apache Maven build tool"
4
4
  classification: LOCAL_EFFECTS
5
5
  strict: false
6
+ subcommand_mode: match_all
7
+ subcommands:
8
+ clean:
9
+ classification: LOCAL_EFFECTS
10
+ risk: LOW
11
+ validate:
12
+ classification: READONLY
13
+ compile:
14
+ classification: LOCAL_EFFECTS
15
+ risk: LOW
16
+ test:
17
+ classification: LOCAL_EFFECTS
18
+ risk: LOW
19
+ test-compile:
20
+ classification: LOCAL_EFFECTS
21
+ risk: LOW
22
+ package:
23
+ classification: LOCAL_EFFECTS
24
+ risk: LOW
25
+ verify:
26
+ classification: LOCAL_EFFECTS
27
+ risk: LOW
28
+ install:
29
+ classification: LOCAL_EFFECTS
30
+ risk: LOW
31
+ site:
32
+ classification: LOCAL_EFFECTS
33
+ risk: LOW
34
+ deploy:
35
+ classification: EXTERNAL_EFFECTS
6
36
  global_options:
7
37
  --help: {overrides: READONLY}
8
38
  -h: {overrides: READONLY}
@@ -3,6 +3,36 @@ command: mvnw
3
3
  description: "Apache Maven wrapper"
4
4
  classification: LOCAL_EFFECTS
5
5
  strict: false
6
+ subcommand_mode: match_all
7
+ subcommands:
8
+ clean:
9
+ classification: LOCAL_EFFECTS
10
+ risk: LOW
11
+ validate:
12
+ classification: READONLY
13
+ compile:
14
+ classification: LOCAL_EFFECTS
15
+ risk: LOW
16
+ test:
17
+ classification: LOCAL_EFFECTS
18
+ risk: LOW
19
+ test-compile:
20
+ classification: LOCAL_EFFECTS
21
+ risk: LOW
22
+ package:
23
+ classification: LOCAL_EFFECTS
24
+ risk: LOW
25
+ verify:
26
+ classification: LOCAL_EFFECTS
27
+ risk: LOW
28
+ install:
29
+ classification: LOCAL_EFFECTS
30
+ risk: LOW
31
+ site:
32
+ classification: LOCAL_EFFECTS
33
+ risk: LOW
34
+ deploy:
35
+ classification: EXTERNAL_EFFECTS
6
36
  global_options:
7
37
  --help: {overrides: READONLY}
8
38
  -h: {overrides: READONLY}
@@ -80,9 +80,11 @@ subcommands:
80
80
  subcommands:
81
81
  pytest:
82
82
  classification: LOCAL_EFFECTS
83
+ risk: LOW
83
84
  strict: false
84
85
  ruff:
85
86
  classification: LOCAL_EFFECTS
87
+ risk: LOW
86
88
  strict: false
87
89
  mypy:
88
90
  classification: READONLY
@@ -14,9 +14,11 @@ subcommands:
14
14
  subcommands:
15
15
  pytest:
16
16
  classification: LOCAL_EFFECTS
17
+ risk: LOW
17
18
  strict: false
18
19
  ruff:
19
20
  classification: LOCAL_EFFECTS
21
+ risk: LOW
20
22
  strict: false
21
23
  mypy:
22
24
  classification: READONLY
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
 
9
9
  import yaml
10
10
 
11
- from .models import Classification, CommandDef, DelegationConfig, DelegationMode, OptionDef, Risk
11
+ from .models import Classification, CommandDef, DelegationConfig, DelegationMode, OptionDef, Risk, SubcommandMode
12
12
 
13
13
 
14
14
  def get_default_commands_dir() -> Path:
@@ -177,6 +177,7 @@ def _parse_command_def(data: dict, command_name: str) -> CommandDef:
177
177
  subcommands = _parse_subcommands(data.get("subcommands", {}))
178
178
  delegates_to = _parse_delegation_config(data.get("delegates_to"))
179
179
  strict = data.get("strict", True)
180
+ subcommand_mode = _parse_subcommand_mode(data.get("subcommand_mode"))
180
181
 
181
182
  return CommandDef(
182
183
  command=command_name,
@@ -186,6 +187,7 @@ def _parse_command_def(data: dict, command_name: str) -> CommandDef:
186
187
  subcommands=subcommands,
187
188
  options=options,
188
189
  strict=strict,
190
+ subcommand_mode=subcommand_mode,
189
191
  delegates_to=delegates_to,
190
192
  )
191
193
 
@@ -204,6 +206,13 @@ def _parse_risk(value: str | None) -> Risk | None:
204
206
  return Risk(value)
205
207
 
206
208
 
209
+ def _parse_subcommand_mode(value: str | None) -> SubcommandMode:
210
+ """Parse a subcommand_mode string into a SubcommandMode enum."""
211
+ if value is None:
212
+ return SubcommandMode.HIERARCHICAL
213
+ return SubcommandMode(value)
214
+
215
+
207
216
  def _parse_options(raw: dict | None) -> dict[str, OptionDef]:
208
217
  """Parse an options map, expanding aliases so each alias maps to the same OptionDef."""
209
218
  if not raw:
@@ -15,6 +15,7 @@ from .models import (
15
15
  DelegationMode,
16
16
  InnerCommandResult,
17
17
  Risk,
18
+ SubcommandMode,
18
19
  )
19
20
 
20
21
  # Shell builtins that are special-cased (not from database)
@@ -101,7 +102,13 @@ def match_command(
101
102
  )
102
103
 
103
104
  # Step 3: Subcommand matching
104
- matched_def, command_chain, remaining = _match_subcommand(remaining, command_def)
105
+ if command_def.subcommand_mode == SubcommandMode.MATCH_ALL:
106
+ matched_names, matched_defs, remaining = _match_all_subcommands(remaining, command_def)
107
+ matched_def = command_def # options come from top-level
108
+ command_chain = matched_names
109
+ else:
110
+ matched_def, command_chain, remaining = _match_subcommand(remaining, command_def)
111
+ matched_defs = [] # not used in hierarchical mode
105
112
 
106
113
  # Step 4: Option classification
107
114
  (
@@ -194,6 +201,33 @@ def match_command(
194
201
  elif all_overrides and not risk_explicitly_set:
195
202
  final_risk = _default_risk(final_classification)
196
203
 
204
+ # Step 5b: For match_all, aggregate subcommand classifications/risks
205
+ if command_def.subcommand_mode == SubcommandMode.MATCH_ALL and matched_defs:
206
+ # Compute the aggregate classification/risk from all matched goals
207
+ agg_classification = Classification.READONLY
208
+ agg_risk = Risk.LOW
209
+ for sub_def in matched_defs:
210
+ sub_class = sub_def.classification or Classification.READONLY
211
+ sub_risk = sub_def.risk if sub_def.risk is not None else _default_risk(sub_class)
212
+ agg_classification = Classification.max_severity(agg_classification, sub_class)
213
+ agg_risk = Risk.max_severity(agg_risk, sub_risk)
214
+
215
+ # Check if there are unrecognized positional args (non-option tokens)
216
+ has_unrecognized_positional = any(not t.startswith("-") for t in remaining_positional)
217
+ if has_unrecognized_positional:
218
+ # Unrecognized goals: include base classification/risk in aggregation
219
+ agg_risk = Risk.max_severity(agg_risk, base_risk)
220
+
221
+ if all_overrides:
222
+ # Option overrides replace base classification; aggregate risk from goals
223
+ # but don't let goals override the option's classification decision
224
+ final_risk = Risk.max_severity(final_risk, agg_risk)
225
+ else:
226
+ # No option overrides: goals determine classification and risk
227
+ final_classification = Classification.max_severity(final_classification, agg_classification)
228
+ final_risk = agg_risk
229
+ classification_reason = f"base classification from rule {matched_rule}"
230
+
197
231
  # Strict mode: unrecognized options -> UNKNOWN
198
232
  if matched_def.strict and unknown_options:
199
233
  final_classification = Classification.max_severity(final_classification, Classification.UNKNOWN)
@@ -386,6 +420,36 @@ def _match_subcommand(
386
420
  return matched_def, command_chain, remaining
387
421
 
388
422
 
423
+ def _match_all_subcommands(
424
+ argv: list[str],
425
+ command_def: CommandDef,
426
+ ) -> tuple[list[str], list[CommandDef], list[str]]:
427
+ """Match each positional arg independently against the same subcommand dict.
428
+
429
+ Used for commands like Maven and Gradle where multiple goals/tasks
430
+ appear as positional args in any order.
431
+
432
+ Returns (matched_names, matched_defs, remaining).
433
+ remaining includes both options and unrecognized positional args.
434
+ """
435
+ matched_names: list[str] = []
436
+ matched_defs: list[CommandDef] = []
437
+ remaining: list[str] = []
438
+
439
+ for token in argv:
440
+ if token.startswith("-"):
441
+ # Options pass through for _classify_options later
442
+ remaining.append(token)
443
+ elif token in command_def.subcommands:
444
+ matched_names.append(token)
445
+ matched_defs.append(command_def.subcommands[token])
446
+ else:
447
+ # Unrecognized goal — pass through as positional
448
+ remaining.append(token)
449
+
450
+ return matched_names, matched_defs, remaining
451
+
452
+
389
453
  def _classify_options(
390
454
  argv: list[str],
391
455
  command_def: CommandDef,
@@ -68,6 +68,13 @@ _RISK_SEVERITY_ORDER: dict[Risk, int] = {
68
68
  }
69
69
 
70
70
 
71
+ class SubcommandMode(enum.Enum):
72
+ """How subcommands are matched against positional arguments."""
73
+
74
+ HIERARCHICAL = "hierarchical"
75
+ MATCH_ALL = "match_all"
76
+
77
+
71
78
  class DelegationMode(enum.Enum):
72
79
  """How a command delegates execution to an inner command."""
73
80
 
@@ -134,6 +141,7 @@ class CommandDef:
134
141
  subcommands: dict[str, CommandDef] = field(default_factory=dict)
135
142
  options: dict[str, OptionDef] = field(default_factory=dict)
136
143
  strict: bool = True
144
+ subcommand_mode: SubcommandMode = SubcommandMode.HIERARCHICAL
137
145
  delegates_to: DelegationConfig | None = None
138
146
 
139
147
 
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from bash_classify.matcher import _is_terminator, _strip_quotes, match_command
6
- from bash_classify.models import Classification, CommandDef, CommandInvocation, Risk
6
+ from bash_classify.models import Classification, CommandDef, CommandInvocation, OptionDef, Risk, SubcommandMode
7
7
 
8
8
 
9
9
  def _make_invocation(argv: list[str]) -> CommandInvocation:
@@ -724,3 +724,77 @@ class TestRiskBuiltins:
724
724
  def test_test_builtin_low_risk(self, database: dict[str, CommandDef]) -> None:
725
725
  result = match_command(_make_invocation(["test", "-f", "file"]), database)
726
726
  assert result.risk == Risk.LOW
727
+
728
+
729
+ class TestMatchAllSubcommandMode:
730
+ """Tests for subcommand_mode: match_all."""
731
+
732
+ def _make_match_all_command(self) -> CommandDef:
733
+ """Create a command with match_all subcommand mode for testing."""
734
+ return CommandDef(
735
+ command="builder",
736
+ classification=Classification.LOCAL_EFFECTS,
737
+ subcommand_mode=SubcommandMode.MATCH_ALL,
738
+ strict=False,
739
+ subcommands={
740
+ "clean": CommandDef(command="clean", classification=Classification.LOCAL_EFFECTS, risk=Risk.LOW),
741
+ "compile": CommandDef(command="compile", classification=Classification.LOCAL_EFFECTS, risk=Risk.LOW),
742
+ "test": CommandDef(command="test", classification=Classification.LOCAL_EFFECTS, risk=Risk.LOW),
743
+ "deploy": CommandDef(command="deploy", classification=Classification.EXTERNAL_EFFECTS),
744
+ },
745
+ options={
746
+ "-B": OptionDef(),
747
+ "--dry-run": OptionDef(overrides=Classification.READONLY),
748
+ },
749
+ )
750
+
751
+ def test_single_known_goal(self) -> None:
752
+ cmd_def = self._make_match_all_command()
753
+ database = {"builder": cmd_def}
754
+ result = match_command(_make_invocation(["builder", "clean"]), database)
755
+ assert result.classification == Classification.LOCAL_EFFECTS
756
+ assert result.risk == Risk.LOW
757
+ assert result.matched_rule == "builder.clean"
758
+
759
+ def test_multiple_known_goals_all_low(self) -> None:
760
+ cmd_def = self._make_match_all_command()
761
+ database = {"builder": cmd_def}
762
+ result = match_command(_make_invocation(["builder", "clean", "compile", "test"]), database)
763
+ assert result.classification == Classification.LOCAL_EFFECTS
764
+ assert result.risk == Risk.LOW
765
+ assert result.matched_rule == "builder.clean.compile.test"
766
+
767
+ def test_multiple_goals_max_classification(self) -> None:
768
+ cmd_def = self._make_match_all_command()
769
+ database = {"builder": cmd_def}
770
+ result = match_command(_make_invocation(["builder", "clean", "deploy"]), database)
771
+ assert result.classification == Classification.EXTERNAL_EFFECTS
772
+ assert result.risk == Risk.MEDIUM # deploy has no explicit risk, EXTERNAL_EFFECTS defaults to MEDIUM
773
+
774
+ def test_unrecognized_goal_uses_base(self) -> None:
775
+ cmd_def = self._make_match_all_command()
776
+ database = {"builder": cmd_def}
777
+ result = match_command(_make_invocation(["builder", "clean", "custom-plugin:goal"]), database)
778
+ assert result.classification == Classification.LOCAL_EFFECTS
779
+ assert result.risk == Risk.MEDIUM # base risk for LOCAL_EFFECTS
780
+
781
+ def test_no_goals_uses_base(self) -> None:
782
+ cmd_def = self._make_match_all_command()
783
+ database = {"builder": cmd_def}
784
+ result = match_command(_make_invocation(["builder"]), database)
785
+ assert result.classification == Classification.LOCAL_EFFECTS
786
+ assert result.risk == Risk.MEDIUM
787
+
788
+ def test_options_still_processed(self) -> None:
789
+ cmd_def = self._make_match_all_command()
790
+ database = {"builder": cmd_def}
791
+ result = match_command(_make_invocation(["builder", "-B", "clean", "compile"]), database)
792
+ assert result.classification == Classification.LOCAL_EFFECTS
793
+ assert result.risk == Risk.LOW
794
+
795
+ def test_option_override_works(self) -> None:
796
+ cmd_def = self._make_match_all_command()
797
+ database = {"builder": cmd_def}
798
+ result = match_command(_make_invocation(["builder", "--dry-run", "clean"]), database)
799
+ assert result.classification == Classification.READONLY
800
+ assert result.risk == Risk.LOW
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from bash_classify.models import Classification, Risk
5
+ from bash_classify.models import Classification, Risk, SubcommandMode
6
6
 
7
7
 
8
8
  class TestClassificationSeverity:
@@ -56,3 +56,9 @@ class TestRiskSeverity:
56
56
  def test_max_severity_all_three(self) -> None:
57
57
  result = Risk.max_severity(Risk.LOW, Risk.MEDIUM, Risk.HIGH)
58
58
  assert result == Risk.HIGH
59
+
60
+
61
+ class TestSubcommandMode:
62
+ def test_subcommand_mode_values(self) -> None:
63
+ assert SubcommandMode.HIERARCHICAL.value == "hierarchical"
64
+ assert SubcommandMode.MATCH_ALL.value == "match_all"
@@ -433,3 +433,35 @@ class TestComplexRealWorld:
433
433
  """Git log piped to xargs for batch show."""
434
434
  result = classify_expression('git log --format="%H" | head -5 | xargs -I{} git show --stat {}', database)
435
435
  assert result.classification == Classification.READONLY
436
+
437
+
438
+ class TestBuildTools:
439
+ """Build tools with match_all subcommand mode (Maven, Gradle)."""
440
+
441
+ def test_mvn_clean_install(self, database):
442
+ result = classify_expression("mvn clean install", database)
443
+ assert result.classification == Classification.LOCAL_EFFECTS
444
+ assert result.risk.value == "LOW"
445
+
446
+ def test_mvn_clean_deploy(self, database):
447
+ result = classify_expression("mvn clean deploy", database)
448
+ assert result.classification == Classification.EXTERNAL_EFFECTS
449
+ assert result.risk.value == "MEDIUM"
450
+
451
+ def test_mvn_custom_plugin_goal(self, database):
452
+ """Unrecognized goal falls back to command base classification."""
453
+ result = classify_expression("mvn clean some-plugin:goal", database)
454
+ assert result.classification == Classification.LOCAL_EFFECTS
455
+
456
+ def test_gradle_clean_build_test(self, database):
457
+ result = classify_expression("gradle clean build test", database)
458
+ assert result.classification == Classification.LOCAL_EFFECTS
459
+ assert result.risk.value == "LOW"
460
+
461
+ def test_gradle_publish(self, database):
462
+ result = classify_expression("gradle clean publish", database)
463
+ assert result.classification == Classification.EXTERNAL_EFFECTS
464
+
465
+ def test_gradle_dry_run(self, database):
466
+ result = classify_expression("gradle --dry-run clean build", database)
467
+ assert result.classification == Classification.READONLY
File without changes
File without changes
File without changes
File without changes