blockwatch-cli 0.15.0__tar.gz → 0.16.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 (270) hide show
  1. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/CHANGELOG.md +33 -0
  2. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/Cargo.lock +1 -1
  3. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/Cargo.toml +1 -1
  4. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/PKG-INFO +1 -1
  5. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/README.md +1 -1
  6. blockwatch_cli-0.16.0/clippy.toml +11 -0
  7. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/ci.md +2 -2
  8. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/cli.md +13 -13
  9. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/check-ai.md +3 -0
  10. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/check-lua.md +3 -0
  11. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/keep-sorted.md +3 -0
  12. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/line-count.md +3 -0
  13. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/same-as.md +46 -26
  14. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/setup.py +1 -1
  15. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/blocks.rs +37 -3
  16. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/config.rs +6 -2
  17. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/fs.rs +5 -1
  18. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/path_arguments.rs +15 -3
  19. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/paths_glob.rs +66 -1
  20. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/settings.rs +17 -1
  21. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/skill.md +27 -23
  22. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/check_ai.rs +27 -1
  23. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/check_lua.rs +33 -0
  24. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/keep_sorted.rs +37 -1
  25. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/line_count.rs +30 -1
  26. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/mod.rs +49 -0
  27. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/same_as.rs +313 -59
  28. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/explain.rs +13 -5
  29. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/same_as.rs +35 -0
  30. blockwatch_cli-0.15.0/clippy.toml +0 -5
  31. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.agents/.claude-plugin/plugin.json +0 -0
  32. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.agents/skills/blockwatch/SKILL.md +0 -0
  33. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.claude-plugin/marketplace.json +0 -0
  34. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/dependabot.yml +0 -0
  35. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/workflows/blockwatch.yml +0 -0
  36. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/workflows/codeql.yml +0 -0
  37. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/workflows/markdownlint.yml +0 -0
  38. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/workflows/pypi.yml +0 -0
  39. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/workflows/release.yml +0 -0
  40. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/workflows/rust.yml +0 -0
  41. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.github/workflows/trusted-checks.yml +0 -0
  42. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.gitignore +0 -0
  43. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.markdownlint-cli2.yaml +0 -0
  44. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.pre-commit-config.yaml +0 -0
  45. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/.pre-commit-hooks.yaml +0 -0
  46. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/AGENTS.md +0 -0
  47. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/CLAUDE.md +0 -0
  48. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/LICENSE +0 -0
  49. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/SECURITY.md +0 -0
  50. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/blockwatch.toml +0 -0
  51. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/codecov.yml +0 -0
  52. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/demo.gif +0 -0
  53. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/dist-workspace.toml +0 -0
  54. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/agents.md +0 -0
  55. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/symbols.md +0 -0
  56. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/README.md +0 -0
  57. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/affects.md +0 -0
  58. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/keep-unique.md +0 -0
  59. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/docs/validators/line-pattern.md +0 -0
  60. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/pypi/pyproject.toml +0 -0
  61. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/pyproject.toml +0 -0
  62. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/release.toml +0 -0
  63. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/scripts/check_language_count.lua +0 -0
  64. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/scripts/check_latest_gpt_nano_model.lua +0 -0
  65. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/block_parser.rs +0 -0
  66. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/cli.rs +0 -0
  67. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/diff_parser.rs +0 -0
  68. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/explain.rs +0 -0
  69. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/flags.rs +0 -0
  70. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/bash.rs +0 -0
  71. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/c.rs +0 -0
  72. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/c_sharp.rs +0 -0
  73. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/cmake.rs +0 -0
  74. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/cpp.rs +0 -0
  75. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/css.rs +0 -0
  76. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/dart.rs +0 -0
  77. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/dockerfile.rs +0 -0
  78. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/elixir.rs +0 -0
  79. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/go.rs +0 -0
  80. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/graphql.rs +0 -0
  81. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/groovy.rs +0 -0
  82. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/hcl.rs +0 -0
  83. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/html.rs +0 -0
  84. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/java.rs +0 -0
  85. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/javascript.rs +0 -0
  86. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/json.rs +0 -0
  87. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/json.scm +0 -0
  88. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/kotlin.rs +0 -0
  89. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/lean.rs +0 -0
  90. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/lua.rs +0 -0
  91. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/makefile.rs +0 -0
  92. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/markdown.rs +0 -0
  93. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/mdx.rs +0 -0
  94. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/mod.rs +0 -0
  95. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/nix.rs +0 -0
  96. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/ocaml.rs +0 -0
  97. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/php.rs +0 -0
  98. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/proto.rs +0 -0
  99. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/python.rs +0 -0
  100. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/ruby.rs +0 -0
  101. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/rust.rs +0 -0
  102. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/scala.rs +0 -0
  103. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/sql.rs +0 -0
  104. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/starlark.rs +0 -0
  105. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/swift.rs +0 -0
  106. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/toml.rs +0 -0
  107. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/tsx.rs +0 -0
  108. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/typescript.rs +0 -0
  109. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/xml.rs +0 -0
  110. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/language_parsers/yaml.rs +0 -0
  111. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/lib.rs +0 -0
  112. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/main.rs +0 -0
  113. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/repo_path.rs +0 -0
  114. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/report.rs +0 -0
  115. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/sarif.rs +0 -0
  116. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/scan.rs +0 -0
  117. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/symbol_path.rs +0 -0
  118. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/symbols.rs +0 -0
  119. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/tag_parser.rs +0 -0
  120. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/target_reference.rs +0 -0
  121. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/affects.rs +0 -0
  122. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/keep_unique.rs +0 -0
  123. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/validators/line_pattern.rs +0 -0
  124. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/violation_address.rs +0 -0
  125. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/src/virtual_blocks.rs +0 -0
  126. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/affects.rs +0 -0
  127. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/check_ai.rs +0 -0
  128. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/check_lua.rs +0 -0
  129. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/common/empty.toml +0 -0
  130. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/common/mod.rs +0 -0
  131. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/config.rs +0 -0
  132. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/encoding.rs +0 -0
  133. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/general.rs +0 -0
  134. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/keep_sorted.rs +0 -0
  135. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/keep_unique.rs +0 -0
  136. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/line_count.rs +0 -0
  137. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/line_pattern.rs +0 -0
  138. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/list.rs +0 -0
  139. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/path_arguments.rs +0 -0
  140. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/repository_root.rs +0 -0
  141. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/sarif.rs +0 -0
  142. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/skill.rs +0 -0
  143. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/suppressions.rs +0 -0
  144. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/symbol_references.rs +0 -0
  145. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/cross_file_source.rs +0 -0
  146. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/cross_file_target.md +0 -0
  147. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/custom_ext_source.javascript +0 -0
  148. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/custom_ext_target.javascript +0 -0
  149. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/dangling.md +0 -0
  150. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/missing_symbol.rs +0 -0
  151. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/more_added_than_deleted.py +0 -0
  152. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/multiline_tag_source.rs +0 -0
  153. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/multiline_tag_target.rs +0 -0
  154. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/nested_mapping_source.rs +0 -0
  155. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/nested_mapping_target.yaml +0 -0
  156. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/sibling.md +0 -0
  157. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/symbol_source.rs +0 -0
  158. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/symbol_target.json +0 -0
  159. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/table_source.rs +0 -0
  160. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/table_target.toml +0 -0
  161. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/tag_and_content_source.ts +0 -0
  162. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/tag_and_content_target.ts +0 -0
  163. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/whole_file_source.rs +0 -0
  164. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/affects/whole_file_target.json +0 -0
  165. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_ai/two_blocks.py +0 -0
  166. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/affects.lua +0 -0
  167. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/affects.py +0 -0
  168. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/affects_drift.py +0 -0
  169. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/blocking_io.lua +0 -0
  170. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/blocking_io.py +0 -0
  171. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/echo.lua +0 -0
  172. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/fail.lua +0 -0
  173. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/mode.py +0 -0
  174. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/os.lua +0 -0
  175. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/pathfmt/report_path.lua +0 -0
  176. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/pathfmt/source.py +0 -0
  177. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/pattern.py +0 -0
  178. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/pattern_many_matches.py +0 -0
  179. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/pattern_no_match.py +0 -0
  180. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/success.lua +0 -0
  181. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/check_lua/success_and_failure.py +0 -0
  182. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/config/glob_target/config.toml +0 -0
  183. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/config/glob_target/package.json +0 -0
  184. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/config/glob_target/plugins/blue/package.json +0 -0
  185. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/config/glob_target/plugins/green/package.json +0 -0
  186. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/config/glob_target/plugins/red/package.json +0 -0
  187. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/explain/a.py +0 -0
  188. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/explain/check.lua +0 -0
  189. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/block_selection.py +0 -0
  190. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/custom_file_extension.javascript +0 -0
  191. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/custom_file_extension.python +0 -0
  192. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/disable_enable.py +0 -0
  193. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/paths/invalid.py +0 -0
  194. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/paths/subdir/nested_invalid.py +0 -0
  195. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/paths/valid.py +0 -0
  196. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/general/severity.py +0 -0
  197. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/hidden/.github/workflows/ci.yml +0 -0
  198. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/keep_sorted/orderings.py +0 -0
  199. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/keep_unique/basic_and_pattern.py +0 -0
  200. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/line_count/equality_and_greater_than.py +0 -0
  201. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/line_pattern/forbidden_text.py +0 -0
  202. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/line_pattern/hex_and_word_patterns.py +0 -0
  203. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/list/.hg/.gitkeep +0 -0
  204. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/list/a.py +0 -0
  205. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/list/b.py +0 -0
  206. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/positions/unicode_columns.py +0 -0
  207. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/custom_ext_source.javascript +0 -0
  208. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/custom_ext_target.javascript +0 -0
  209. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/package.json +0 -0
  210. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/paths/alpha/item.txt +0 -0
  211. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/paths/beta/item.txt +0 -0
  212. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/paths_and_block_source.md +0 -0
  213. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/paths_source.md +0 -0
  214. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/sibling.rs +0 -0
  215. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/source.rs +0 -0
  216. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/split_table_source.rs +0 -0
  217. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/split_table_target.toml +0 -0
  218. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/symbol_mismatch_source.rs +0 -0
  219. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/symbol_source.rs +0 -0
  220. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/target.md +0 -0
  221. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/whole_file_mismatch_source.rs +0 -0
  222. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/whole_file_source.rs +0 -0
  223. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/same_as/whole_file_target.json +0 -0
  224. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/sarif/advisory.py +0 -0
  225. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/sarif/clean.py +0 -0
  226. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/sarif/sorted.py +0 -0
  227. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/sarif/unnamed.py +0 -0
  228. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/commit_msg.txt +0 -0
  229. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/commit_msg_gone_a.txt +0 -0
  230. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/commit_msg_gone_b.txt +0 -0
  231. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/commit_msg_lowercase_trailer.txt +0 -0
  232. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/commit_msg_malformed.txt +0 -0
  233. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/commit_msg_with_noise.txt +0 -0
  234. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/dangling.py +0 -0
  235. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/sorted.py +0 -0
  236. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/symbol.py +0 -0
  237. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/symbol_target.json +0 -0
  238. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/two_rules.py +0 -0
  239. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/suppressions/unnamed.py +0 -0
  240. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/escapes_source.rs +0 -0
  241. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/escapes_target.json +0 -0
  242. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/filter_source.rs +0 -0
  243. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/filter_target.yaml +0 -0
  244. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/json_source.rs +0 -0
  245. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/json_target.json +0 -0
  246. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/jsonc_source.rs +0 -0
  247. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/jsonc_target.jsonc +0 -0
  248. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/markdown_source.rs +0 -0
  249. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/markdown_target.markdown +0 -0
  250. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/md_source.rs +0 -0
  251. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/md_target.md +0 -0
  252. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/mdx_source.rs +0 -0
  253. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/mdx_target.mdx +0 -0
  254. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/mixed_list_source.rs +0 -0
  255. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/mixed_list_target.json +0 -0
  256. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/toml_source.rs +0 -0
  257. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/toml_target.toml +0 -0
  258. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/yaml_source.rs +0 -0
  259. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/yaml_target.yaml +0 -0
  260. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/yml_source.rs +0 -0
  261. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/symbol_references/yml_target.yml +0 -0
  262. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/affects/pair.py +0 -0
  263. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/clean/counted.py +0 -0
  264. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/clean/noop.py +0 -0
  265. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/clean/notes.txt +0 -0
  266. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/clean/sorted.py +0 -0
  267. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/diff/two_blocks.py +0 -0
  268. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/failing/unsorted.py +0 -0
  269. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/testdata/verbosity/mixed/both_kinds.py +0 -0
  270. {blockwatch_cli-0.15.0 → blockwatch_cli-0.16.0}/tests/verbosity.rs +0 -0
@@ -12,6 +12,39 @@ Releases up to and including v0.3.11 predate this file. Their notes live on the
12
12
 
13
13
  ## [Unreleased] - ReleaseDate
14
14
 
15
+ ## [0.16.0] - 2026-10-24
16
+
17
+ ### Changed
18
+
19
+ - Without `same-as-pattern`, `same-as` compares each line that isn't blank as one value. Before, the whole block was
20
+ one value, so `same-as-mode` and `same-as-format` didn't work on a block of several lines. For example,
21
+ `same-as-mode="single"` passed with two lines, `subset` failed on a line that the target had, and `numeric` reported
22
+ that `1` and `2` together are not a number ([#179](https://github.com/mennanov/blockwatch/issues/179)).
23
+ - Without `same-as-mode`, a block without `same-as-pattern` compares in `sequence` mode. So it still fails when its
24
+ lines differ from the target's or come in another order. A block with a pattern still compares in `set` mode.
25
+ - A block without `same-as-pattern` now fails when its target's pattern finds a value more than once, or in another
26
+ order: `["1.2.3"] != ["1.2.3", "1.2.3"]`. Before, the default `set` mode ignored this. To keep the old result, add
27
+ `same-as-mode="set"`, or `same-as-mode="subset"` when the target may hold other values too.
28
+ - `same-as` now fails on two empty blocks, as it does on a pattern that matches nothing. The message for both is
29
+ `this block or its target has no values to compare`.
30
+ - `same-as-paths` no longer needs `same-as-pattern`. Without one, each line of the block must be a path, in sorted
31
+ order.
32
+ - An attribute that changes how a validator works now stops the run on a block without the validator's main
33
+ attribute. Before, it did nothing there. For example, `same-as-mode="sequence"` on a target let values in another
34
+ order pass. The attributes are:
35
+ - `same-as-mode` and `same-as-format`, without `same-as` or `same-as-paths`;
36
+ - `same-as-pattern`, without `same-as`, `same-as-paths` or `name`, because only a block with a name can be a target;
37
+ - `keep-sorted-pattern` and `keep-sorted-format`, without `keep-sorted`;
38
+ - `line-count-pattern`, without `line-count`;
39
+ - `check-ai-pattern`, without `check-ai`;
40
+ - `check-lua-pattern` and `check-lua-timeout`, without `check-lua`.
41
+
42
+ ### Fixed
43
+
44
+ - An empty alternative in braces now matches, so `{,sub/}a.yml` matches both `a.yml` and `sub/a.yml`. Before, it
45
+ matched only `sub/a.yml`, with no error. This was true of every glob: a `[[block]]` target, `same-as-paths`, the
46
+ `ignore` setting and `--ignore`, and a path argument ([#177](https://github.com/mennanov/blockwatch/issues/177)).
47
+
15
48
  ## [0.15.0] - 2026-10-21
16
49
 
17
50
  ### Changed
@@ -274,7 +274,7 @@ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
274
274
 
275
275
  [[package]]
276
276
  name = "blockwatch"
277
- version = "0.15.0"
277
+ version = "0.16.0"
278
278
  dependencies = [
279
279
  "anyhow",
280
280
  "assert-json-diff",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "blockwatch"
3
- version = "0.15.0"
3
+ version = "0.16.0"
4
4
  edition = "2024"
5
5
  repository = "https://github.com/mennanov/blockwatch"
6
6
  description = "Language agnostic linter that keeps your code and documentation in sync and valid"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blockwatch-cli
3
- Version: 0.15.0
3
+ Version: 0.16.0
4
4
  Summary: Language agnostic linter that keeps your code and documentation in sync and valid
5
5
  Keywords: linter,sync,drift,comments,agents
6
6
  Home-Page: https://github.com/mennanov/blockwatch
@@ -218,7 +218,7 @@ exclusions, and custom extension mappings.
218
218
 
219
219
  ```yaml
220
220
  - repo: https://github.com/mennanov/blockwatch
221
- rev: v0.15.0 # Use latest release
221
+ rev: v0.16.0 # Use latest release
222
222
  hooks:
223
223
  - id: blockwatch
224
224
  ```
@@ -0,0 +1,11 @@
1
+ # The integration tests run inside this repository, so a run would read its blockwatch.toml, which
2
+ # skips tests/testdata.
3
+ disallowed-macros = [
4
+ { path = "assert_cmd::cargo_bin_cmd", reason = "use common::cargo_bin_cmd!, which passes an empty config" },
5
+ ]
6
+ # globset drops an empty alternative in braces by default, so `{,sub/}a.yml` would not match
7
+ # `a.yml`.
8
+ disallowed-methods = [
9
+ { path = "globset::Glob::new", reason = "use paths_glob::glob_builder, which keeps an empty alternative" },
10
+ { path = "globset::GlobBuilder::new", reason = "use paths_glob::glob_builder, which keeps an empty alternative" },
11
+ ]
@@ -19,7 +19,7 @@ To use [pre-commit](https://pre-commit.com), add the hook to `.pre-commit-config
19
19
 
20
20
  ```yaml
21
21
  - repo: https://github.com/mennanov/blockwatch
22
- rev: v0.15.0 # Use latest release
22
+ rev: v0.16.0 # Use latest release
23
23
  hooks:
24
24
  - id: blockwatch
25
25
  ```
@@ -59,7 +59,7 @@ The `blockwatch` hook runs before the commit message exists, so it cannot see a
59
59
 
60
60
  ```yaml
61
61
  - repo: https://github.com/mennanov/blockwatch
62
- rev: v0.15.0 # Use latest release
62
+ rev: v0.16.0 # Use latest release
63
63
  hooks:
64
64
  - id: blockwatch-commit-msg
65
65
  ```
@@ -557,15 +557,15 @@ Nothing else about the run changes: the same violations are found, and the exit
557
557
  "tool": {
558
558
  "driver": {
559
559
  "name": "blockwatch",
560
- "version": "0.15.0",
561
- "semanticVersion": "0.15.0",
560
+ "version": "0.16.0",
561
+ "semanticVersion": "0.16.0",
562
562
  "informationUri": "https://github.com/mennanov/blockwatch",
563
563
  "rules": [
564
564
  {
565
565
  "id": "keep-sorted",
566
566
  "name": "keep-sorted",
567
567
  "shortDescription": { "text": "Requires the lines of a block to stay in order." },
568
- "helpUri": "https://github.com/mennanov/blockwatch/blob/v0.15.0/docs/validators/keep-sorted.md"
568
+ "helpUri": "https://github.com/mennanov/blockwatch/blob/v0.16.0/docs/validators/keep-sorted.md"
569
569
  }
570
570
  ]
571
571
  }
@@ -790,16 +790,16 @@ A trace stops where its validator stops, such as at the first line out of order.
790
790
  file, and `text` is that line trimmed, as the validator compares it. A key that holds what a violation reports has the
791
791
  name the violation's `data` gives it, such as `reason`.
792
792
 
793
- | Validator | What it shows |
794
- |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
795
- | `same-as` | `records`: each match of `same-as-pattern` in the block, with its `line`, the text the regex matched (`match`), and each named group after `same-as-format` (`groups`). `targets`: the `records` of each target, and the `reason` of its violation, or `null` when the two agree. `paths`: with `same-as-paths`, the `glob`, the paths it `matched`, their `records` and the `reason`. |
796
- | `keep-sorted` | `lines`: each line read, with the `value` it compares, or `null` when `keep-sorted-pattern` skips the line. `format` and `order`. |
797
- | `keep-unique` | `lines`, as for `keep-sorted`. A repeated value has `duplicate_of`: the line where it first appeared. |
798
- | `line-pattern` | `lines`: each line read, with the text `line-pattern` matched (`match`). A line that fails has `fails`: the check it fails. `forbidden_match` is the text `line-pattern-forbidden` found. |
799
- | `line-count` | `lines`, as for `keep-sorted`. A line counts when its `value` isn't `null`. The number of lines counted (`actual`), and the rule (`op` and `expected`). |
800
- | `affects` | Whether the diff changed the block (`block_changed`). For each target: whether the diff `changed` it, or the `reason` it doesn't resolve. |
801
- | `check-lua` | The `content` and the `affects` the script got, and the message it returned (`lua_error`), or `null` when it passed. `affects` is `ctx.affects` without the content of each target. |
802
- | `check-ai` | The `model`, the `prompt` sent and the `response`. Both are `null` when `check-ai-pattern` matched nothing, so no request was sent. |
793
+ | Validator | What it shows |
794
+ |----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
795
+ | `same-as` | `records`: each match of `same-as-pattern` in the block, or each line without a pattern, with its `line`, the text the regex matched (`match`), and each named group after `same-as-format` (`groups`). `mode`: the `same-as-mode` it compared with. `targets`: the `records` of each target, and the `reason` of its violation, or `null` when the two agree. `paths`: with `same-as-paths`, the `glob`, the paths it `matched`, their `records` and the `reason`. |
796
+ | `keep-sorted` | `lines`: each line read, with the `value` it compares, or `null` when `keep-sorted-pattern` skips the line. `format` and `order`. |
797
+ | `keep-unique` | `lines`, as for `keep-sorted`. A repeated value has `duplicate_of`: the line where it first appeared. |
798
+ | `line-pattern` | `lines`: each line read, with the text `line-pattern` matched (`match`). A line that fails has `fails`: the check it fails. `forbidden_match` is the text `line-pattern-forbidden` found. |
799
+ | `line-count` | `lines`, as for `keep-sorted`. A line counts when its `value` isn't `null`. The number of lines counted (`actual`), and the rule (`op` and `expected`). |
800
+ | `affects` | Whether the diff changed the block (`block_changed`). For each target: whether the diff `changed` it, or the `reason` it doesn't resolve. |
801
+ | `check-lua` | The `content` and the `affects` the script got, and the message it returned (`lua_error`), or `null` when it passed. `affects` is `ctx.affects` without the content of each target. |
802
+ | `check-ai` | The `model`, the `prompt` sent and the `response`. Both are `null` when `check-ai-pattern` matched nothing, so no request was sent. |
803
803
 
804
804
  ## Run Reports
805
805
 
@@ -10,6 +10,9 @@ mention the company name", "no TODOs left", "the tone matches the rest of the pa
10
10
  | `check-ai` | the condition, in natural language | — |
11
11
  | `check-ai-pattern` | regex; every match's `(?P<value>…)` group, or whole match | whole block |
12
12
 
13
+ `check-ai-pattern` works only next to `check-ai`. On a block without it, it stops the run, because it would do nothing
14
+ there.
15
+
13
16
  ## Example
14
17
 
15
18
  ```html
@@ -19,6 +19,9 @@ string when it fails.
19
19
 
20
20
  <!-- </block> -->
21
21
 
22
+ `check-lua-pattern` and `check-lua-timeout` work only next to `check-lua`. On a block without it, they stop the run,
23
+ because they would do nothing there.
24
+
22
25
  The script path must point to a file inside the repository. Paths that escape it — absolute paths outside the project,
23
26
  `../` traversal, or symlinks pointing outward — are rejected.
24
27
 
@@ -10,6 +10,9 @@ Keeps a list ordered. Eliminates "please sort this" review nits.
10
10
  | `keep-sorted-pattern` | regex; the `(?P<value>…)` group, or the whole match | whole line |
11
11
  | `keep-sorted-format` | `numeric` | text |
12
12
 
13
+ `keep-sorted-pattern` and `keep-sorted-format` work only next to `keep-sorted`. On a block without it, they stop the
14
+ run, because they would do nothing there.
15
+
13
16
  ## Example
14
17
 
15
18
  ```python
@@ -10,6 +10,9 @@ size of the enum it maps.
10
10
  | `line-count` | `<N`, `<=N`, `==N`, `>=N`, `>N` | — |
11
11
  | `line-count-pattern` | regex; a line counts when it gives a value | every line that has text |
12
12
 
13
+ `line-count-pattern` works only next to `line-count`. On a block without it, it stops the run, because it would do
14
+ nothing there.
15
+
13
16
  ## Example
14
17
 
15
18
  ```python
@@ -8,13 +8,17 @@ It runs on every scan, with or without a diff.
8
8
 
9
9
  ## Syntax
10
10
 
11
- | Attribute | Value | Default |
12
- |-------------------|--------------------------------------------------------------------------------------|-------------------|
13
- | `same-as` | One or more [targets](#targets), comma-separated | — |
14
- | `same-as-paths` | A [glob](#paths-as-targets) of paths to compare | — |
15
- | `same-as-pattern` | A regex that picks the values to compare | the whole content |
16
- | `same-as-mode` | `set`, `sequence`, `single` or `subset` | `set` |
17
- | `same-as-format` | `numeric`, `identifier` or `flat`, or [one for each group](#a-format-for-each-group) | text |
11
+ | Attribute | Value | Default |
12
+ |-------------------|--------------------------------------------------------------------------------------|------------------------------------------|
13
+ | `same-as` | One or more [targets](#targets), comma-separated | — |
14
+ | `same-as-paths` | A [glob](#paths-as-targets) of paths to compare | — |
15
+ | `same-as-pattern` | A regex that picks the values to compare | each line |
16
+ | `same-as-mode` | `set`, `sequence`, `single` or `subset` | [`sequence` or `set`](#comparison-modes) |
17
+ | `same-as-format` | `numeric`, `identifier` or `flat`, or [one for each group](#a-format-for-each-group) | text |
18
+
19
+ `same-as-mode` and `same-as-format` work only next to `same-as` or `same-as-paths`. `same-as-pattern` also works next
20
+ to `name`, because a block with a name can be a target. On any other block, they stop the run, because they would do
21
+ nothing there.
18
22
 
19
23
  ## Targets
20
24
 
@@ -28,7 +32,8 @@ It runs on every scan, with or without a diff.
28
32
 
29
33
  ## Example
30
34
 
31
- With no other attributes, both blocks must hold **identical** text. Blank lines and indentation don't count. This suits
35
+ With no other attributes, both blocks must hold **identical** text: the same lines in the same order. Blank lines and
36
+ indentation don't count. This suits
32
37
  text you can't share any other way, such as a command shown in two docs:
33
38
 
34
39
  **README.md**:
@@ -145,7 +150,8 @@ pub const NODE_VERSION: &str = "20.11.1";
145
150
  ```
146
151
 
147
152
  The file has no block of its own, so the referencing block's pattern reads both sides. For anything longer than one
148
- line, you will want a pattern: without one, the block must equal the entire file.
153
+ line, you will want a pattern: without one, the block must equal the entire file. Or add `same-as-mode="subset"`: then
154
+ each line of the block must be a line of the file.
149
155
 
150
156
  ## Symbols as targets
151
157
 
@@ -187,10 +193,10 @@ show every entry of a directory, such as the services in a README:
187
193
  The glob lists `services/auth/`, `services/billing/` and `services/search/`. The pattern picks `auth`, `billing` and
188
194
  `search` from them, and `billing` and `search` from the block. So the run reports that `auth` is missing.
189
195
 
190
- - **`same-as-pattern` is required.** It reads both the block and the paths. To compare whole lines, use
191
- `same-as-pattern=".+"`.
192
- - **The paths are sorted, and each one is listed once.** So `same-as-mode="sequence"` also checks that the block
193
- lists them in that order.
196
+ - **The block's `same-as-pattern` reads both the block and the paths.** Without a pattern, each line of the block must
197
+ be a path, such as `services/billing/`.
198
+ - **The paths are sorted, and each one is listed once.** Without a pattern, or with `same-as-mode="sequence"`, the
199
+ block must list them in that order. To allow any order, add `same-as-mode="set"`.
194
200
  - **A trailing `/` lists directories.** A directory is listed when it holds a file, and `services/billing/` is listed
195
201
  once however many files it holds. Without the `/`, the glob lists files: `crates/*/AGENTS.md`.
196
202
  - **`*` stops at `/`.** `services/*/` lists `services/billing/`, but not `services/billing/src/`. `**` matches any
@@ -205,12 +211,22 @@ A block can have both `same-as` and `same-as-paths`. Each target is compared wit
205
211
 
206
212
  ## Comparison modes
207
213
 
208
- | `same-as-mode` | Passes when |
209
- |-----------------|-------------------------------------------------------------------|
210
- | `set` (default) | both sides hold the same values, in any order, ignoring repeats |
211
- | `sequence` | both sides hold the same values in the same order |
212
- | `single` | each side holds exactly one value, and the two are equal |
213
- | `subset` | every value in this block also appears in the target |
214
+ | `same-as-mode` | Passes when |
215
+ |----------------|-----------------------------------------------------------------|
216
+ | `set` | both sides hold the same values, in any order, ignoring repeats |
217
+ | `sequence` | both sides hold the same values in the same order |
218
+ | `single` | each side holds exactly one value, and the two are equal |
219
+ | `subset` | every value in this block also appears in the target |
220
+
221
+ Without `same-as-mode`, the mode depends on the block's own `same-as-pattern`. A target's pattern doesn't change it.
222
+
223
+ - **The block has no pattern:** `sequence`. Each line that isn't blank is a value, so the two sides must hold the same
224
+ lines in the same order, as in the [example](#example).
225
+ - **The block has a pattern:** `set`.
226
+
227
+ So adding a pattern stops order from mattering. To keep the order, add `same-as-mode="sequence"` too. And a block
228
+ without a pattern fails when its target's pattern finds a value twice: `["1.2.3"] != ["1.2.3", "1.2.3"]`. To allow
229
+ repeats, add `same-as-mode="set"` or `same-as-mode="subset"`.
214
230
 
215
231
  `subset` checks one direction only. Use it when one side should hold part of the other, like a test that uses only
216
232
  some of the environment variables:
@@ -356,14 +372,18 @@ const TIMEOUT_SECS: f64 = 2.0;
356
372
  ## Notes
357
373
 
358
374
  - **Which block's attributes count.** The referencing block's `same-as-mode` and `same-as-format` decide how to compare.
359
- Each block's `same-as-pattern` reads only that block. A whole file, a symbol or a list of paths has no block, so the
360
- referencing block's pattern reads it too.
375
+ On a target, or any block without `same-as` or `same-as-paths`, they stop the run, because they would do nothing
376
+ there. Each block's `same-as-pattern` reads only that block. A whole file, a symbol or a list of paths has no block,
377
+ so the referencing block's pattern reads it too. Only a block with a name can be a target, so `same-as-pattern` on a
378
+ block without `same-as`, `same-as-paths` or `name` stops the run.
361
379
  - **Violations:** a missing block, a missing or repeated key, a value that is not a number under `numeric`, a `single`
362
- side without exactly one value, a pattern that matches nothing on either side, two patterns with different groups,
363
- and a `same-as-paths` glob that matches no path. Two empty sides do not count as equal.
364
- - **Errors that stop the run:** an unknown `same-as-mode` or `same-as-format`, a `same-as-format` entry for a group
365
- that the pattern doesn't have, an invalid regex or glob, `same-as-paths` without `same-as-pattern`, a missing target
366
- file, and a symbol in a file with a syntax error, without symbols, or with several YAML documents.
380
+ side without exactly one value, a side with no values (such as an empty block, or a pattern that matches nothing),
381
+ two patterns with different groups, and a `same-as-paths` glob that matches no path. Two empty sides do not count as
382
+ equal.
383
+ - **Errors that stop the run:** an unknown `same-as-mode` or `same-as-format`, either of them on a block without
384
+ `same-as` or `same-as-paths`, `same-as-pattern` on a block without `same-as`, `same-as-paths` or `name`, a
385
+ `same-as-format` entry for a group that the pattern doesn't have, an invalid regex or glob, a missing target file,
386
+ and a symbol in a file with a syntax error, without symbols, or with several YAML documents.
367
387
  - **Run it on the whole tree now and then.** `same-as` needs no diff, so a plain `blockwatch` run catches drift that
368
388
  `--only-changed` misses. See [CI integration](../ci.md).
369
389
  - **A rename alone goes unnoticed in a diff.** Renaming a target file without changing it (a plain `git mv`) gives
@@ -6,7 +6,7 @@ setup(
6
6
  name='blockwatch-pre-commit-placeholder',
7
7
  version='0.0.0',
8
8
  # <block name="blockwatch-cli-version" same-as-pattern="blockwatch-cli==(?P<value>\d+\.\d+\.\d+)">
9
- install_requires=['blockwatch-cli==0.15.0'],
9
+ install_requires=['blockwatch-cli==0.16.0'],
10
10
  # </block>
11
11
  # Without it, setuptools takes the directories under src/ for Python packages.
12
12
  packages=[],
@@ -13,6 +13,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
13
13
  use std::fmt;
14
14
  use std::ops::Range;
15
15
  use std::str::FromStr;
16
+ use strum::IntoEnumIterator;
16
17
  use strum_macros::EnumString;
17
18
 
18
19
  const UNNAMED_BLOCK_LABEL: &str = "(unnamed)";
@@ -496,11 +497,12 @@ const RECOGNIZED_ATTRIBUTES: &[&str] = &[
496
497
  // </block>
497
498
  ];
498
499
 
499
- /// Checks that each attribute of a block is recognized. It does not check the values: see
500
+ /// Checks that each attribute of a block is recognized, and that each validator accepts the
501
+ /// attributes it reads (see [`Validator::check_attributes`]). It does not check the values: see
500
502
  /// [`BlockSeverity::from_attributes`] for `severity`.
501
503
  ///
502
504
  /// # Errors
503
- /// Returns an error that quotes the attribute that is not recognized, such as
505
+ /// Returns an error that quotes the first attribute found to be wrong, such as
504
506
  /// ``unrecognized attribute `keep-sortd` ``.
505
507
  pub(crate) fn validate_attributes(attributes: &HashMap<String, String>) -> anyhow::Result<()> {
506
508
  if let Some(name) = attributes
@@ -509,7 +511,39 @@ pub(crate) fn validate_attributes(attributes: &HashMap<String, String>) -> anyho
509
511
  {
510
512
  bail!("unrecognized attribute `{name}`");
511
513
  }
512
- Ok(())
514
+ // A block can be in a file that the run reads only to find a target, and no validator runs on
515
+ // such a block. Every parsed block passes here.
516
+ Validator::iter().try_for_each(|validator| validator.check_attributes(attributes))
517
+ }
518
+
519
+ #[cfg(test)]
520
+ mod validate_attributes_tests {
521
+ use crate::blocks::{RECOGNIZED_ATTRIBUTES, validate_attributes};
522
+ use crate::validators::Validator;
523
+ use std::collections::HashMap;
524
+ use strum::IntoEnumIterator;
525
+
526
+ #[test]
527
+ fn single_attribute_returns_an_error_only_when_it_needs_another() {
528
+ // A new attribute must either work alone or stop the run alone. Otherwise a block with
529
+ // only that attribute would pass without being checked.
530
+ let needs_no_other: Vec<&str> = Validator::iter()
531
+ .map(Validator::as_str)
532
+ .chain([
533
+ "line-pattern-forbidden",
534
+ "name",
535
+ "same-as-paths",
536
+ "severity",
537
+ ])
538
+ .collect();
539
+ for name in RECOGNIZED_ATTRIBUTES {
540
+ let attributes = HashMap::from([(name.to_string(), String::new())]);
541
+
542
+ let result = validate_attributes(&attributes);
543
+
544
+ assert_eq!(result.is_ok(), needs_no_other.contains(name), "{name}");
545
+ }
546
+ }
513
547
  }
514
548
 
515
549
  #[cfg(test)]
@@ -272,7 +272,9 @@ fn is_entry_modified(
272
272
  /// - the target's file is a glob that is not valid or that matches no file;
273
273
  /// - `target-pattern` comes with a symbol, or is not a valid regex;
274
274
  /// - an attribute is not one a tag can have, or has a value that is not a string, `true` or an
275
- /// integer.
275
+ /// integer;
276
+ /// - an attribute comes without the attribute its validator needs, such as `check-lua-timeout`
277
+ /// without `check-lua`.
276
278
  fn entry_blocks(
277
279
  block_entry: BlockEntry,
278
280
  entry: ConfigEntry,
@@ -433,6 +435,7 @@ mod tests {
433
435
  target = 'package.json#/version'
434
436
  name = 'version'
435
437
  keep-unique = true
438
+ check-lua = 'check.lua'
436
439
  check-lua-timeout = 30
437
440
 
438
441
  [[block]]
@@ -450,6 +453,7 @@ target = 'a.yaml#/b'
450
453
  attributes: HashMap::from([
451
454
  ("name".to_string(), "version".to_string()),
452
455
  ("keep-unique".to_string(), String::new()),
456
+ ("check-lua".to_string(), "check.lua".to_string()),
453
457
  ("check-lua-timeout".to_string(), "30".to_string()),
454
458
  ]),
455
459
  entry: ConfigEntry {
@@ -465,7 +469,7 @@ target = 'a.yaml#/b'
465
469
  attributes: HashMap::new(),
466
470
  entry: ConfigEntry {
467
471
  file: PathBuf::from(DEFAULT_FILE),
468
- line: 9,
472
+ line: 10,
469
473
  },
470
474
  is_entry_modified: false,
471
475
  severity: BlockSeverity::Error,
@@ -263,7 +263,11 @@ impl PathFilter {
263
263
  fn glob_set(globs: impl IntoIterator<Item = impl AsRef<str>>) -> GlobSet {
264
264
  let mut builder = GlobSet::builder();
265
265
  for glob in globs {
266
- builder.add(globset::Glob::new(glob.as_ref()).expect("malformed test glob"));
266
+ builder.add(
267
+ crate::paths_glob::glob_builder(glob.as_ref())
268
+ .build()
269
+ .expect("malformed test glob"),
270
+ );
267
271
  }
268
272
  builder.build().expect("failed to build test glob set")
269
273
  }
@@ -1,9 +1,10 @@
1
1
  //! The paths and globs given on the command line, and the check that each selects a file.
2
2
 
3
3
  use crate::fs::{Entry, FileSystem};
4
+ use crate::paths_glob::glob_builder;
4
5
  use crate::repo_path::RepoPath;
5
6
  use anyhow::{Context, bail};
6
- use globset::{Glob, GlobSet, GlobSetBuilder};
7
+ use globset::{GlobSet, GlobSetBuilder};
7
8
  use std::collections::BTreeSet;
8
9
  use std::path::Path;
9
10
 
@@ -42,7 +43,9 @@ impl PathArguments {
42
43
  None => argument.trim_start_matches("./").to_string(),
43
44
  };
44
45
  builder.add(
45
- Glob::new(&glob).with_context(|| format!("invalid glob pattern: {argument}"))?,
46
+ glob_builder(&glob)
47
+ .build()
48
+ .with_context(|| format!("invalid glob pattern: {argument}"))?,
46
49
  );
47
50
  }
48
51
  Ok(Self {
@@ -55,7 +58,7 @@ impl PathArguments {
55
58
  /// no arguments, because then every file is checked.
56
59
  pub(crate) fn allowed_globs(&self) -> anyhow::Result<GlobSet> {
57
60
  if self.written.is_empty() {
58
- return Ok(GlobSet::new([Glob::new("**")?])?);
61
+ return Ok(GlobSet::new([glob_builder("**").build()?])?);
59
62
  }
60
63
  Ok(self.globs.clone())
61
64
  }
@@ -180,6 +183,15 @@ mod tests {
180
183
  Ok(())
181
184
  }
182
185
 
186
+ #[test]
187
+ fn glob_argument_with_empty_alternative_selects_the_path_without_it() -> anyhow::Result<()> {
188
+ assert_eq!(
189
+ selected_files(&["a.py", "sub/a.py", "other/a.py"], &["{,sub/}a.py"])?,
190
+ ["a.py", "sub/a.py"]
191
+ );
192
+ Ok(())
193
+ }
194
+
183
195
  #[test]
184
196
  fn error_in_files_ensure_each_matches_returns_the_error() -> anyhow::Result<()> {
185
197
  let error = resolve_globs(&["src/**"])?
@@ -9,6 +9,20 @@ use std::collections::BTreeSet;
9
9
  /// path it spells.
10
10
  pub(crate) const GLOB_CHARACTERS: [char; 5] = ['*', '?', '[', '{', '\\'];
11
11
 
12
+ /// Starts building `glob`, so that an empty alternative in braces matches the empty string:
13
+ /// `{,sub/}a.yml` matches both `a.yml` and `sub/a.yml`.
14
+ pub(crate) fn glob_builder(glob: &str) -> GlobBuilder<'_> {
15
+ // globset drops an empty alternative by default, so `{,sub/}a.yml` would match only
16
+ // `sub/a.yml`. Shells and most other glob tools match both.
17
+ #[expect(
18
+ clippy::disallowed_methods,
19
+ reason = "the one place that sets empty_alternates"
20
+ )]
21
+ let mut builder = GlobBuilder::new(glob);
22
+ builder.empty_alternates(true);
23
+ builder
24
+ }
25
+
12
26
  /// A glob over the repository's paths, compiled to list the files or the directories it matches.
13
27
  pub(crate) struct PathsGlob {
14
28
  /// Matches a path written without a trailing `/`.
@@ -25,7 +39,7 @@ impl PathsGlob {
25
39
  pub(crate) fn new(glob: &str) -> anyhow::Result<Self> {
26
40
  // `*` stops at `/`, so `services/*` matches the services and not the directories inside
27
41
  // them.
28
- let matcher = GlobBuilder::new(glob)
42
+ let matcher = glob_builder(glob)
29
43
  .literal_separator(true)
30
44
  .build()?
31
45
  .compile_matcher();
@@ -93,3 +107,54 @@ fn base_directory(glob: &str) -> Option<RepoPath> {
93
107
  // the walk. Walking the whole repository gives the same paths, only more slowly.
94
108
  RepoPath::from_reference(&fixed_segments.join("/")).ok()
95
109
  }
110
+
111
+ #[cfg(test)]
112
+ mod tests {
113
+ use super::*;
114
+ use crate::fs::test_utils::FakeFileSystem;
115
+
116
+ /// The files that `glob` matches in a repository that holds `files`, sorted.
117
+ fn matching_files(glob: &str, files: &[&str]) -> anyhow::Result<Vec<String>> {
118
+ let file_system = FakeFileSystem::new(
119
+ files
120
+ .iter()
121
+ .map(|file| (file.to_string(), String::new()))
122
+ .collect(),
123
+ );
124
+ Ok(PathsGlob::new(glob)?
125
+ .matching_files(&file_system)?
126
+ .iter()
127
+ .map(ToString::to_string)
128
+ .collect())
129
+ }
130
+
131
+ #[test]
132
+ fn empty_first_alternative_matches_the_path_without_it() -> anyhow::Result<()> {
133
+ assert_eq!(
134
+ matching_files(
135
+ "{,sub/}a.yml",
136
+ &["a.yml", "sub/a.yml", "other/a.yml", "b.yml"]
137
+ )?,
138
+ ["a.yml", "sub/a.yml"]
139
+ );
140
+ Ok(())
141
+ }
142
+
143
+ #[test]
144
+ fn empty_last_alternative_matches_the_path_without_it() -> anyhow::Result<()> {
145
+ assert_eq!(
146
+ matching_files("a{.yml,}", &["a", "a.yml", "a.json", "ab"])?,
147
+ ["a", "a.yml"]
148
+ );
149
+ Ok(())
150
+ }
151
+
152
+ #[test]
153
+ fn empty_nested_alternative_matches_the_path_without_it() -> anyhow::Result<()> {
154
+ assert_eq!(
155
+ matching_files("{x,{,y}}a", &["a", "xa", "ya", "za"])?,
156
+ ["a", "xa", "ya"]
157
+ );
158
+ Ok(())
159
+ }
160
+ }
@@ -1,6 +1,7 @@
1
1
  //! Project settings: how they are validated, and how the flags and the config file are merged.
2
2
 
3
3
  use crate::blocks::BlockAddress;
4
+ use crate::paths_glob::glob_builder;
4
5
  use crate::validators::Validator;
5
6
  use anyhow::{Context, bail};
6
7
  use globset::{Glob, GlobSet, GlobSetBuilder};
@@ -36,7 +37,9 @@ impl RawSettings {
36
37
  .ignore
37
38
  .iter()
38
39
  .map(|glob| {
39
- Glob::new(glob).with_context(|| format!("invalid ignore glob pattern: {glob}"))
40
+ glob_builder(glob)
41
+ .build()
42
+ .with_context(|| format!("invalid ignore glob pattern: {glob}"))
40
43
  })
41
44
  .collect::<anyhow::Result<_>>()?;
42
45
  let extensions = self
@@ -286,6 +289,19 @@ mod tests {
286
289
  Ok(())
287
290
  }
288
291
 
292
+ #[test]
293
+ fn ignore_glob_with_empty_alternative_ignores_the_path_without_it() -> anyhow::Result<()> {
294
+ let config_file = RawSettings {
295
+ ignore: to_string_vec(&["{,sub/}a.rs"]),
296
+ ..RawSettings::default()
297
+ };
298
+ let settings = resolve(RawSettings::default(), config_file)?;
299
+ assert!(settings.ignored_globs.is_match("a.rs"));
300
+ assert!(settings.ignored_globs.is_match("sub/a.rs"));
301
+ assert!(!settings.ignored_globs.is_match("other/a.rs"));
302
+ Ok(())
303
+ }
304
+
289
305
  #[test]
290
306
  fn extensions_in_flags_and_config_file_are_combined() -> anyhow::Result<()> {
291
307
  let flags = RawSettings {