blockwatch-cli 0.11.3__tar.gz → 0.13.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 (263) hide show
  1. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/AGENTS.md +15 -7
  2. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/CHANGELOG.md +56 -0
  3. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/CLAUDE.md +15 -7
  4. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/Cargo.lock +1 -1
  5. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/Cargo.toml +1 -1
  6. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/PKG-INFO +1 -1
  7. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/README.md +4 -4
  8. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/blockwatch.toml +40 -0
  9. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/ci.md +2 -2
  10. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/cli.md +4 -3
  11. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/symbols.md +49 -5
  12. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/keep-sorted.md +2 -1
  13. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/keep-unique.md +2 -1
  14. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/same-as.md +3 -2
  15. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/setup.py +1 -1
  16. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/block_parser.rs +13 -0
  17. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/blocks.rs +8 -2
  18. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/cli.rs +8 -7
  19. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/flags.rs +35 -3
  20. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/bash.rs +2 -1
  21. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/cmake.rs +7 -2
  22. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/css.rs +5 -4
  23. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/json.rs +4 -2
  24. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/lean.rs +2 -0
  25. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/lua.rs +6 -1
  26. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/markdown.rs +516 -39
  27. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/mdx.rs +63 -9
  28. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/mod.rs +59 -31
  29. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/ocaml.rs +2 -2
  30. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/php.rs +3 -3
  31. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/ruby.rs +7 -1
  32. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/rust.rs +6 -5
  33. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/scala.rs +15 -0
  34. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/sql.rs +6 -5
  35. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/toml.rs +8 -0
  36. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/yaml.rs +24 -4
  37. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/lib.rs +17 -1
  38. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/paths_glob.rs +1 -0
  39. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/sarif.rs +9 -15
  40. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/skill.md +31 -14
  41. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/symbol_path.rs +58 -4
  42. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/symbols.rs +20 -11
  43. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/tag_parser.rs +31 -4
  44. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/check_ai.rs +20 -0
  45. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/check_lua.rs +33 -0
  46. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/keep_unique.rs +19 -0
  47. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/line_pattern.rs +8 -0
  48. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/mod.rs +149 -31
  49. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/same_as.rs +22 -2
  50. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/virtual_blocks.rs +71 -5
  51. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/general.rs +25 -0
  52. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/skill.rs +3 -3
  53. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/symbol_references.rs +21 -0
  54. blockwatch_cli-0.13.0/tests/testdata/symbol_references/markdown_source.rs +3 -0
  55. blockwatch_cli-0.13.0/tests/testdata/symbol_references/markdown_target.markdown +5 -0
  56. blockwatch_cli-0.13.0/tests/testdata/symbol_references/md_source.rs +3 -0
  57. blockwatch_cli-0.13.0/tests/testdata/symbol_references/md_target.md +5 -0
  58. blockwatch_cli-0.13.0/tests/testdata/symbol_references/mdx_source.rs +3 -0
  59. blockwatch_cli-0.13.0/tests/testdata/symbol_references/mdx_target.mdx +5 -0
  60. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.agents/.claude-plugin/plugin.json +0 -0
  61. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.agents/skills/blockwatch/SKILL.md +0 -0
  62. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.claude-plugin/marketplace.json +0 -0
  63. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/dependabot.yml +0 -0
  64. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/workflows/blockwatch.yml +0 -0
  65. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/workflows/codeql.yml +0 -0
  66. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/workflows/markdownlint.yml +0 -0
  67. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/workflows/pypi.yml +0 -0
  68. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/workflows/release.yml +0 -0
  69. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/workflows/rust.yml +0 -0
  70. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.github/workflows/trusted-checks.yml +0 -0
  71. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.gitignore +0 -0
  72. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.markdownlint-cli2.yaml +0 -0
  73. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.pre-commit-config.yaml +0 -0
  74. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/.pre-commit-hooks.yaml +0 -0
  75. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/LICENSE +0 -0
  76. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/SECURITY.md +0 -0
  77. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/clippy.toml +0 -0
  78. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/codecov.yml +0 -0
  79. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/demo.gif +0 -0
  80. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/dist-workspace.toml +0 -0
  81. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/agents.md +0 -0
  82. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/README.md +0 -0
  83. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/affects.md +0 -0
  84. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/check-ai.md +0 -0
  85. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/check-lua.md +0 -0
  86. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/line-count.md +0 -0
  87. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/docs/validators/line-pattern.md +0 -0
  88. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/pypi/pyproject.toml +0 -0
  89. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/pyproject.toml +0 -0
  90. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/release.toml +0 -0
  91. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/scripts/check_language_count.lua +0 -0
  92. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/scripts/check_latest_gpt_nano_model.lua +0 -0
  93. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/config.rs +0 -0
  94. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/diff_parser.rs +0 -0
  95. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/fs.rs +0 -0
  96. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/c.rs +0 -0
  97. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/c_sharp.rs +0 -0
  98. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/cpp.rs +0 -0
  99. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/dart.rs +0 -0
  100. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/dockerfile.rs +0 -0
  101. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/elixir.rs +0 -0
  102. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/go.rs +0 -0
  103. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/graphql.rs +0 -0
  104. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/groovy.rs +0 -0
  105. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/hcl.rs +0 -0
  106. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/html.rs +0 -0
  107. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/java.rs +0 -0
  108. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/javascript.rs +0 -0
  109. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/json.scm +0 -0
  110. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/kotlin.rs +0 -0
  111. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/makefile.rs +0 -0
  112. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/nix.rs +0 -0
  113. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/proto.rs +0 -0
  114. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/python.rs +0 -0
  115. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/starlark.rs +0 -0
  116. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/swift.rs +0 -0
  117. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/tsx.rs +0 -0
  118. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/typescript.rs +0 -0
  119. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/language_parsers/xml.rs +0 -0
  120. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/main.rs +0 -0
  121. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/path_arguments.rs +0 -0
  122. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/repo_path.rs +0 -0
  123. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/report.rs +0 -0
  124. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/settings.rs +0 -0
  125. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/affects.rs +0 -0
  126. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/keep_sorted.rs +0 -0
  127. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/validators/line_count.rs +0 -0
  128. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/src/violation_address.rs +0 -0
  129. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/affects.rs +0 -0
  130. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/check_ai.rs +0 -0
  131. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/check_lua.rs +0 -0
  132. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/common/empty.toml +0 -0
  133. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/common/mod.rs +0 -0
  134. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/config.rs +0 -0
  135. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/encoding.rs +0 -0
  136. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/keep_sorted.rs +0 -0
  137. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/keep_unique.rs +0 -0
  138. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/line_count.rs +0 -0
  139. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/line_pattern.rs +0 -0
  140. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/list.rs +0 -0
  141. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/path_arguments.rs +0 -0
  142. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/repository_root.rs +0 -0
  143. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/same_as.rs +0 -0
  144. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/sarif.rs +0 -0
  145. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/suppressions.rs +0 -0
  146. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/cross_file_source.rs +0 -0
  147. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/cross_file_target.md +0 -0
  148. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/custom_ext_source.javascript +0 -0
  149. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/custom_ext_target.javascript +0 -0
  150. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/dangling.md +0 -0
  151. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/missing_symbol.rs +0 -0
  152. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/more_added_than_deleted.py +0 -0
  153. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/multiline_tag_source.rs +0 -0
  154. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/multiline_tag_target.rs +0 -0
  155. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/nested_mapping_source.rs +0 -0
  156. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/nested_mapping_target.yaml +0 -0
  157. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/sibling.md +0 -0
  158. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/symbol_source.rs +0 -0
  159. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/symbol_target.json +0 -0
  160. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/table_source.rs +0 -0
  161. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/table_target.toml +0 -0
  162. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/tag_and_content_source.ts +0 -0
  163. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/tag_and_content_target.ts +0 -0
  164. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/whole_file_source.rs +0 -0
  165. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/affects/whole_file_target.json +0 -0
  166. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_ai/two_blocks.py +0 -0
  167. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/affects.lua +0 -0
  168. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/affects.py +0 -0
  169. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/affects_drift.py +0 -0
  170. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/blocking_io.lua +0 -0
  171. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/blocking_io.py +0 -0
  172. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/echo.lua +0 -0
  173. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/fail.lua +0 -0
  174. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/mode.py +0 -0
  175. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/os.lua +0 -0
  176. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/pathfmt/report_path.lua +0 -0
  177. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/pathfmt/source.py +0 -0
  178. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/pattern.py +0 -0
  179. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/pattern_many_matches.py +0 -0
  180. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/pattern_no_match.py +0 -0
  181. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/success.lua +0 -0
  182. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/check_lua/success_and_failure.py +0 -0
  183. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/config/glob_target/config.toml +0 -0
  184. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/config/glob_target/package.json +0 -0
  185. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/config/glob_target/plugins/blue/package.json +0 -0
  186. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/config/glob_target/plugins/green/package.json +0 -0
  187. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/config/glob_target/plugins/red/package.json +0 -0
  188. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/block_selection.py +0 -0
  189. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/custom_file_extension.javascript +0 -0
  190. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/custom_file_extension.python +0 -0
  191. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/disable_enable.py +0 -0
  192. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/paths/invalid.py +0 -0
  193. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/paths/subdir/nested_invalid.py +0 -0
  194. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/paths/valid.py +0 -0
  195. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/general/severity.py +0 -0
  196. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/hidden/.github/workflows/ci.yml +0 -0
  197. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/keep_sorted/orderings.py +0 -0
  198. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/keep_unique/basic_and_pattern.py +0 -0
  199. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/line_count/equality_and_greater_than.py +0 -0
  200. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/line_pattern/forbidden_text.py +0 -0
  201. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/line_pattern/hex_and_word_patterns.py +0 -0
  202. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/list/.hg/.gitkeep +0 -0
  203. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/list/a.py +0 -0
  204. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/list/b.py +0 -0
  205. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/positions/unicode_columns.py +0 -0
  206. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/custom_ext_source.javascript +0 -0
  207. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/custom_ext_target.javascript +0 -0
  208. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/package.json +0 -0
  209. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/paths/alpha/item.txt +0 -0
  210. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/paths/beta/item.txt +0 -0
  211. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/paths_and_block_source.md +0 -0
  212. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/paths_source.md +0 -0
  213. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/sibling.rs +0 -0
  214. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/source.rs +0 -0
  215. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/split_table_source.rs +0 -0
  216. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/split_table_target.toml +0 -0
  217. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/symbol_mismatch_source.rs +0 -0
  218. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/symbol_source.rs +0 -0
  219. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/target.md +0 -0
  220. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/whole_file_mismatch_source.rs +0 -0
  221. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/whole_file_source.rs +0 -0
  222. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/same_as/whole_file_target.json +0 -0
  223. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/sarif/advisory.py +0 -0
  224. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/sarif/clean.py +0 -0
  225. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/sarif/sorted.py +0 -0
  226. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/sarif/unnamed.py +0 -0
  227. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/commit_msg.txt +0 -0
  228. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/commit_msg_gone_a.txt +0 -0
  229. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/commit_msg_gone_b.txt +0 -0
  230. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/commit_msg_lowercase_trailer.txt +0 -0
  231. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/commit_msg_malformed.txt +0 -0
  232. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/commit_msg_with_noise.txt +0 -0
  233. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/dangling.py +0 -0
  234. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/sorted.py +0 -0
  235. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/symbol.py +0 -0
  236. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/symbol_target.json +0 -0
  237. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/two_rules.py +0 -0
  238. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/suppressions/unnamed.py +0 -0
  239. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/escapes_source.rs +0 -0
  240. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/escapes_target.json +0 -0
  241. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/filter_source.rs +0 -0
  242. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/filter_target.yaml +0 -0
  243. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/json_source.rs +0 -0
  244. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/json_target.json +0 -0
  245. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/jsonc_source.rs +0 -0
  246. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/jsonc_target.jsonc +0 -0
  247. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/mixed_list_source.rs +0 -0
  248. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/mixed_list_target.json +0 -0
  249. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/toml_source.rs +0 -0
  250. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/toml_target.toml +0 -0
  251. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/yaml_source.rs +0 -0
  252. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/yaml_target.yaml +0 -0
  253. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/yml_source.rs +0 -0
  254. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/symbol_references/yml_target.yml +0 -0
  255. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/affects/pair.py +0 -0
  256. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/clean/counted.py +0 -0
  257. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/clean/noop.py +0 -0
  258. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/clean/notes.txt +0 -0
  259. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/clean/sorted.py +0 -0
  260. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/diff/two_blocks.py +0 -0
  261. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/failing/unsorted.py +0 -0
  262. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/testdata/verbosity/mixed/both_kinds.py +0 -0
  263. {blockwatch_cli-0.11.3 → blockwatch_cli-0.13.0}/tests/verbosity.rs +0 -0
@@ -62,7 +62,10 @@ For example:
62
62
  - *Prefer* the harness's editing tools to one-off scripts.
63
63
  - *Always* handle the variants of an enum with an exhaustive `match`. A `let ... else`, or an `if` that handles one
64
64
  variant and sends the rest to a single branch, quietly gives any new variant whatever that branch does. With a
65
- `match`, the compiler makes you decide what to do with each new variant.
65
+ `match`, the compiler makes you decide what to do with each new variant. One exception: a `match` on another
66
+ crate's enum can end with `_` when it only asks whether a value is one variant, and no new variant could be that
67
+ one. For example, every Markdown heading is `mdast::Node::Heading`, so the other node types can go to `_ => None`.
68
+ Say why in a comment next to the `_`.
66
69
  - *Always* give an item the narrowest visibility that compiles: private, then `pub(super)`, then `pub(crate)`.
67
70
  blockwatch is a program, not a library. Only `run` and `count_rust_blocks` in `lib.rs` are `pub`, for `main.rs` and
68
71
  the fuzz target. The `unreachable_pub` lint makes `cargo clippy -- -D warnings` fail on any other `pub`.
@@ -200,8 +203,9 @@ The main modules:
200
203
  - `src/virtual_blocks.rs` — `VirtualBlock`, a block that the config file declares in one file, instead of tags in a
201
204
  comment. A file that can't or must not hold a tag, such as `package.json` or a prompt, can only get blocks this way.
202
205
  It is a block of the file it wraps. Its `Locator` says how to find it:
203
- - `Symbol`: the content is what a reference to the symbol reads: a scalar's decoded value
204
- (`ContentText::Decoded`), or else the definition's text.
206
+ - `Symbol`: the content is what a reference to the symbol reads: its value, or else the definition's text. A value
207
+ written as is, such as a number or a Markdown section, keeps its place in the file. A value with quotes or escapes
208
+ is decoded (`ContentText::Decoded`), so every line of it reports the value's start.
205
209
  - `Pattern`: the `target-pattern` regex must match once. The content is its `value` group, or else the whole match.
206
210
  - `File`: the content is the whole file.
207
211
 
@@ -224,6 +228,8 @@ The main modules:
224
228
  syntax tree is nested.
225
229
  - YAML walks its syntax tree itself. An alias has a position in a sequence but can't be addressed, and a query can't
226
230
  express that.
231
+ - Markdown finds its sections with the `markdown` crate. The `section` nodes of `tree-sitter-md` don't end at a
232
+ heading underlined with `---`.
227
233
 
228
234
  Each language's tests list every symbol it finds. When you give a language symbols, also document its paths in
229
235
  `docs/symbols.md` and in the "Symbols" section of the skill, `src/skill.md`. A test in `language_parsers/mod.rs` and the
@@ -269,8 +275,10 @@ skill this project ships, so that agents can add blocks to **other** repositorie
269
275
  They are not instructions for working on blockwatch, and changing them changes what users get. This file is the
270
276
  guidance for working on blockwatch.
271
277
 
272
- A block tag in `src/skill.md` can have a name and a pattern, but no rule. A project that saves the skill parses its
273
- tags too, and a rule's target exists only in this repository. Put the rule on the block at the other end instead.
278
+ `src/skill.md` must not hold block tags. A project that saves the skill parses it too, so each tag would become a block
279
+ in that project. To point a block at a section of the skill, put the section between two plain comments, such as
280
+ `<!-- json-paths -->` and `<!-- /json-paths -->`. Then add a `[[block]]` entry that matches them to `blockwatch.toml`,
281
+ next to the others for `src/skill.md`.
274
282
 
275
283
  Each agent's local config directory is left out of git on purpose: `.claude/`, `.cursor/`, `.gemini/`, `.windsurf/`,
276
284
  `.aider*`, and the others listed in `.gitignore`. Don't commit agent-specific settings. Anything a future contributor
@@ -279,8 +287,8 @@ needs belongs in this file.
279
287
  ## Release
280
288
 
281
289
  The GitHub Actions workflows in `.github/workflows/` build releases with `cargo-dist`, configured in
282
- `dist-workspace.toml`. The version is bumped in `Cargo.toml`, in a commit titled
283
- `chore: Release blockwatch version X.Y.Z`.
290
+ `dist-workspace.toml`. `cargo release` bumps the version in `Cargo.toml`, in a commit titled `chore: Release`, and
291
+ tags that commit `vX.Y.Z`.
284
292
 
285
293
  `pypi.yml` builds the same tag for PyPI, as `blockwatch-cli`. The pre-commit hooks install that package through the
286
294
  root `setup.py`. So if the upload for a tag fails, the hooks at that tag can't install.
@@ -12,6 +12,62 @@ 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.13.0] - 2026-10-15
16
+
17
+ ### Added
18
+
19
+ - A symbol path can point at a section of a Markdown file (`.md`, `.markdown`, `.mdx`) by its headings. For example,
20
+ `docs/cli.md#/CLI Reference/Run Modes` is the `## Run Modes` section under the `# CLI Reference` title. So
21
+ `affects` and `same-as` can point at a section, and a `[[block]]` entry can wrap one, with no tags in the file. See
22
+ [Markdown](docs/symbols.md#markdown) ([#175](https://github.com/mennanov/blockwatch/issues/175)).
23
+
24
+ ### Changed
25
+
26
+ - Messages now show a space inside a key of a symbol path as written. Markdown headings almost always hold spaces, so
27
+ a path to a section reads like the file: `docs/cli.md#/CLI Reference/Run Modes`, not
28
+ `docs/cli.md#/CLI%20Reference/Run%20Modes`. A space at the start or end of a key still shows as `%20`. In any file,
29
+ an `affects` or `same-as` violation on a key with a space gets a new hash, so update any `--suppress` address with
30
+ four parts that points at it.
31
+
32
+ ### Fixed
33
+
34
+ - The skill that `blockwatch skill` prints no longer adds blocks to the project that saves it. Before, a saved skill
35
+ added six named blocks, such as `validator-catalog` and `json-paths`, to `blockwatch list` and to the
36
+ `--verbosity summary` counts. Save the skill again with `blockwatch skill` to remove them
37
+ ([#174](https://github.com/mennanov/blockwatch/issues/174)).
38
+
39
+ ## [0.12.0] - 2026-10-15
40
+
41
+ ### Changed
42
+
43
+ - A `blockwatch-suppress:` line with no address now fails the run with `invalid suppression address`. Before,
44
+ blockwatch ignored it.
45
+ - Messages now show a non-ASCII key in a symbol path as written, such as `data.toml#/täble/key`. Before, they showed
46
+ `data.toml#/t%C3%A4ble/key`. An `affects` or `same-as` violation on such a key gets a new hash, so update any
47
+ `--suppress` address with four parts that points at it.
48
+
49
+ ### Fixed
50
+
51
+ - `--suppress-from` no longer crashes on a line with a non-ASCII character, such as `❌` or `—`, near its start.
52
+ Before, such a line could make blockwatch exit with code 101, even when the code had no violations
53
+ ([#172](https://github.com/mennanov/blockwatch/issues/172)).
54
+ - blockwatch no longer crashes on a file that holds a tag and one of these:
55
+ - in Scala, a `/*` comment that is never closed;
56
+ - in Markdown, a `[//]:` line with no title, such as `[//]: #`, or with a quote in its destination, such as
57
+ `[//]: it's (text)`.
58
+ - In Markdown, a tag in a `[//]:` title that starts on the next line is now reported on that line. Before, it was
59
+ reported on the `[//]:` line.
60
+ - A Lua error in a `check-lua` script now shows the script and the line, such as `check.lua:3:`. Before, it showed a
61
+ file of blockwatch's own source code: `src/validators/check_lua.rs:309:3:`.
62
+ - `blockwatch list`, the JSON violations and the SARIF log now come out in the same order on every run. Files are
63
+ sorted by path, and the violations in a file by line, column and validator. Before, the order changed from run to
64
+ run.
65
+ - A pattern with its `value` group in one branch, such as `^(?:none|(?P<value>\w+))$`, now skips a match of the other
66
+ branch, such as `none`. This applies to `keep-sorted-pattern`, `keep-unique`, `same-as-pattern`, `check-ai-pattern`
67
+ and `check-lua-pattern`. Before, such a match gave its whole text as a value, so `keep-unique` could report a false
68
+ duplicate ([#173](https://github.com/mennanov/blockwatch/issues/173)). A `target-pattern` match like that now stops
69
+ the run. Before, the block's content was the whole match.
70
+
15
71
  ## [0.11.3] - 2026-10-15
16
72
 
17
73
  ### Fixed
@@ -62,7 +62,10 @@ For example:
62
62
  - *Prefer* the harness's editing tools to one-off scripts.
63
63
  - *Always* handle the variants of an enum with an exhaustive `match`. A `let ... else`, or an `if` that handles one
64
64
  variant and sends the rest to a single branch, quietly gives any new variant whatever that branch does. With a
65
- `match`, the compiler makes you decide what to do with each new variant.
65
+ `match`, the compiler makes you decide what to do with each new variant. One exception: a `match` on another
66
+ crate's enum can end with `_` when it only asks whether a value is one variant, and no new variant could be that
67
+ one. For example, every Markdown heading is `mdast::Node::Heading`, so the other node types can go to `_ => None`.
68
+ Say why in a comment next to the `_`.
66
69
  - *Always* give an item the narrowest visibility that compiles: private, then `pub(super)`, then `pub(crate)`.
67
70
  blockwatch is a program, not a library. Only `run` and `count_rust_blocks` in `lib.rs` are `pub`, for `main.rs` and
68
71
  the fuzz target. The `unreachable_pub` lint makes `cargo clippy -- -D warnings` fail on any other `pub`.
@@ -200,8 +203,9 @@ The main modules:
200
203
  - `src/virtual_blocks.rs` — `VirtualBlock`, a block that the config file declares in one file, instead of tags in a
201
204
  comment. A file that can't or must not hold a tag, such as `package.json` or a prompt, can only get blocks this way.
202
205
  It is a block of the file it wraps. Its `Locator` says how to find it:
203
- - `Symbol`: the content is what a reference to the symbol reads: a scalar's decoded value
204
- (`ContentText::Decoded`), or else the definition's text.
206
+ - `Symbol`: the content is what a reference to the symbol reads: its value, or else the definition's text. A value
207
+ written as is, such as a number or a Markdown section, keeps its place in the file. A value with quotes or escapes
208
+ is decoded (`ContentText::Decoded`), so every line of it reports the value's start.
205
209
  - `Pattern`: the `target-pattern` regex must match once. The content is its `value` group, or else the whole match.
206
210
  - `File`: the content is the whole file.
207
211
 
@@ -224,6 +228,8 @@ The main modules:
224
228
  syntax tree is nested.
225
229
  - YAML walks its syntax tree itself. An alias has a position in a sequence but can't be addressed, and a query can't
226
230
  express that.
231
+ - Markdown finds its sections with the `markdown` crate. The `section` nodes of `tree-sitter-md` don't end at a
232
+ heading underlined with `---`.
227
233
 
228
234
  Each language's tests list every symbol it finds. When you give a language symbols, also document its paths in
229
235
  `docs/symbols.md` and in the "Symbols" section of the skill, `src/skill.md`. A test in `language_parsers/mod.rs` and the
@@ -269,8 +275,10 @@ skill this project ships, so that agents can add blocks to **other** repositorie
269
275
  They are not instructions for working on blockwatch, and changing them changes what users get. This file is the
270
276
  guidance for working on blockwatch.
271
277
 
272
- A block tag in `src/skill.md` can have a name and a pattern, but no rule. A project that saves the skill parses its
273
- tags too, and a rule's target exists only in this repository. Put the rule on the block at the other end instead.
278
+ `src/skill.md` must not hold block tags. A project that saves the skill parses it too, so each tag would become a block
279
+ in that project. To point a block at a section of the skill, put the section between two plain comments, such as
280
+ `<!-- json-paths -->` and `<!-- /json-paths -->`. Then add a `[[block]]` entry that matches them to `blockwatch.toml`,
281
+ next to the others for `src/skill.md`.
274
282
 
275
283
  Each agent's local config directory is left out of git on purpose: `.claude/`, `.cursor/`, `.gemini/`, `.windsurf/`,
276
284
  `.aider*`, and the others listed in `.gitignore`. Don't commit agent-specific settings. Anything a future contributor
@@ -279,8 +287,8 @@ needs belongs in this file.
279
287
  ## Release
280
288
 
281
289
  The GitHub Actions workflows in `.github/workflows/` build releases with `cargo-dist`, configured in
282
- `dist-workspace.toml`. The version is bumped in `Cargo.toml`, in a commit titled
283
- `chore: Release blockwatch version X.Y.Z`.
290
+ `dist-workspace.toml`. `cargo release` bumps the version in `Cargo.toml`, in a commit titled `chore: Release`, and
291
+ tags that commit `vX.Y.Z`.
284
292
 
285
293
  `pypi.yml` builds the same tag for PyPI, as `blockwatch-cli`. The pre-commit hooks install that package through the
286
294
  root `setup.py`. So if the upload for a tag fails, the hooks at that tag can't install.
@@ -274,7 +274,7 @@ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
274
274
 
275
275
  [[package]]
276
276
  name = "blockwatch"
277
- version = "0.11.3"
277
+ version = "0.13.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.11.3"
3
+ version = "0.13.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.11.3
3
+ Version: 0.13.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
@@ -88,9 +88,9 @@ Two validators work across files:
88
88
  - `affects` fails when a diff touches a block without also changing what it points to.
89
89
  - `same-as` fails when a block and its target hold different values. It needs no diff.
90
90
 
91
- Both can point to a block, a whole file, or a specific value in a file like `package.json#/version`
92
- (see [Symbols](docs/symbols.md)). The other validators check a single block on their own: the things you'd otherwise
93
- have to nitpick in code review.
91
+ Both can point to a block, a whole file, a specific value in a file like `package.json#/version`, or a section of a
92
+ Markdown file (see [Symbols](docs/symbols.md)). The other validators check a single block on their own: the things
93
+ you'd otherwise have to nitpick in code review.
94
94
 
95
95
  <!-- <block name="available-validators"
96
96
  same-as="src/validators/mod.rs:validator-registry, docs/validators/README.md:validators-index"
@@ -214,7 +214,7 @@ exclusions, and custom extension mappings.
214
214
 
215
215
  ```yaml
216
216
  - repo: https://github.com/mennanov/blockwatch
217
- rev: v0.11.3 # Use latest release
217
+ rev: v0.13.0 # Use latest release
218
218
  hooks:
219
219
  - id: blockwatch
220
220
  ```
@@ -38,3 +38,43 @@ same-as = '''.github/workflows/codeql.yml,
38
38
  .github/workflows/pypi.yml,
39
39
  .github/workflows/rust.yml'''
40
40
  same-as-pattern = '(?:actions/checkout@|^)(?P<value>v\d+)$'
41
+
42
+ # `blockwatch skill` prints src/skill.md, and projects save that output. A tag in that file would become a block in
43
+ # every one of those projects. So the file marks its sections with plain comments, which are not tags, and these
44
+ # entries make each marked section a named block.
45
+ [[block]]
46
+ target = 'src/skill.md'
47
+ target-pattern = '(?s)<!-- validator-catalog -->.*<!-- /validator-catalog -->'
48
+ name = 'validator-catalog'
49
+
50
+ [[block]]
51
+ target = 'src/skill.md'
52
+ target-pattern = '(?s)<!-- validator-reference -->.*<!-- /validator-reference -->'
53
+ name = 'validator-reference'
54
+ same-as-pattern = '^\|\s*`(?P<value>[a-z-]+)`'
55
+
56
+ [[block]]
57
+ target = 'src/skill.md'
58
+ target-pattern = '(?s)<!-- extensions-with-symbols -->.*<!-- /extensions-with-symbols -->'
59
+ name = 'extensions-with-symbols'
60
+ same-as-pattern = '`\.?(?P<value>[A-Za-z]+)`'
61
+
62
+ [[block]]
63
+ target = 'src/skill.md'
64
+ target-pattern = '(?s)<!-- json-paths -->.*<!-- /json-paths -->'
65
+ name = 'json-paths'
66
+
67
+ [[block]]
68
+ target = 'src/skill.md'
69
+ target-pattern = '(?s)<!-- toml-paths -->.*<!-- /toml-paths -->'
70
+ name = 'toml-paths'
71
+
72
+ [[block]]
73
+ target = 'src/skill.md'
74
+ target-pattern = '(?s)<!-- yaml-paths -->.*<!-- /yaml-paths -->'
75
+ name = 'yaml-paths'
76
+
77
+ [[block]]
78
+ target = 'src/skill.md'
79
+ target-pattern = '(?s)<!-- markdown-paths -->.*<!-- /markdown-paths -->'
80
+ name = 'markdown-paths'
@@ -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.11.3 # Use latest release
22
+ rev: v0.13.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.11.3 # Use latest release
62
+ rev: v0.13.0 # Use latest release
63
63
  hooks:
64
64
  - id: blockwatch-commit-msg
65
65
  ```
@@ -394,6 +394,7 @@ same-as-pattern = 'os\.environ\["(?P<value>[A-Z_]+)"\]'
394
394
  - **A match can span lines with `(?s)`.** `(?s)^CREATE TABLE users \(.*?^\);` matches the whole statement. Write
395
395
  `.*?`, not `.*`. A `.*` runs to the last `);` in the file, and that still counts as one match.
396
396
  - **A `value` group picks the content**, as in the `*-pattern` attributes. Without one, the content is the whole match.
397
+ A match of a branch without the group stops the run, such as `MODEL = none` for `^MODEL = (?:none|"(?P<value>.+)")`.
397
398
 
398
399
  A virtual block is a block of the file it wraps:
399
400
 
@@ -554,15 +555,15 @@ Nothing else about the run changes: the same violations are found, and the exit
554
555
  "tool": {
555
556
  "driver": {
556
557
  "name": "blockwatch",
557
- "version": "0.11.3",
558
- "semanticVersion": "0.11.3",
558
+ "version": "0.13.0",
559
+ "semanticVersion": "0.13.0",
559
560
  "informationUri": "https://github.com/mennanov/blockwatch",
560
561
  "rules": [
561
562
  {
562
563
  "id": "keep-sorted",
563
564
  "name": "keep-sorted",
564
565
  "shortDescription": { "text": "Requires the lines of a block to stay in order." },
565
- "helpUri": "https://github.com/mennanov/blockwatch/blob/v0.11.3/docs/validators/keep-sorted.md"
566
+ "helpUri": "https://github.com/mennanov/blockwatch/blob/v0.13.0/docs/validators/keep-sorted.md"
566
567
  }
567
568
  ]
568
569
  }
@@ -24,11 +24,12 @@ declares, instead of tags in a comment.
24
24
  <!-- <block name="extensions-with-symbols" same-as="src/language_parsers/mod.rs:extensions-with-symbols"
25
25
  same-as-pattern="`\.?(?P<value>[A-Za-z]+)`"> -->
26
26
 
27
- | Language | Files | Paths |
28
- |----------|-----------------------------------|---------------|
29
- | JSON | `.json`, `.jsonc`, `.webmanifest` | [JSON](#json) |
30
- | TOML | `.toml`, `Pipfile` | [TOML](#toml) |
31
- | YAML | `.yaml`, `.yml`, `.cff` | [YAML](#yaml) |
27
+ | Language | Files | Paths |
28
+ |----------|-----------------------------------|-----------------------|
29
+ | JSON | `.json`, `.jsonc`, `.webmanifest` | [JSON](#json) |
30
+ | TOML | `.toml`, `Pipfile` | [TOML](#toml) |
31
+ | YAML | `.yaml`, `.yml`, `.cff` | [YAML](#yaml) |
32
+ | Markdown | `.md`, `.markdown`, `.mdx` | [Markdown](#markdown) |
32
33
 
33
34
  <!-- </block> -->
34
35
 
@@ -189,6 +190,49 @@ jobs:
189
190
 
190
191
  <!-- </block> -->
191
192
 
193
+ ## Markdown
194
+
195
+ <!-- <block name="markdown-paths"> -->
196
+
197
+ Every section is a symbol. A section is a heading and everything up to the next heading of the same or a higher level.
198
+ Its path is the text of each heading it is under, then its own:
199
+
200
+ ```markdown
201
+ # Guide
202
+
203
+ ## Install
204
+
205
+ Run `cargo install blockwatch`.
206
+
207
+ ### From source
208
+
209
+ Clone the repository.
210
+ ```
211
+
212
+ | Path | Points at |
213
+ |-------------------------------|---------------------------------------------|
214
+ | `#/Guide` | everything below `# Guide` |
215
+ | `#/Guide/Install` | `Run …` and the whole `From source` section |
216
+ | `#/Guide/Install/From source` | `Clone the repository.` |
217
+
218
+ - **The top `#` title is part of every path**, as above.
219
+ - **A section holds its subsections.** `affects` counts a change to any of its lines, the heading included.
220
+ - **A section's value is its text without the heading line.** `same-as` compares that text. The blank lines around it
221
+ don't count.
222
+ - **A heading counts as a reader sees it.** ``## The `--diff` flag`` is `#/The --diff flag`. Code, emphasis and links
223
+ lose their markup.
224
+ - **Headings underlined with `===` or `---` count.** A `#` line in a code block doesn't. Neither does a heading in a
225
+ quote, a list item or an HTML block.
226
+ - **The same heading twice under one parent is a violation**, because blockwatch can't tell which one you mean. Two
227
+ `### Example` sections under different `##` headings are fine.
228
+ - **Escape a heading like any key.** `## Input/Output` is `#/Input~1Output`, and `## Step 1: build` is
229
+ `#/Step 1%3A build`.
230
+ - **An `.mdx` file gets the same paths.** The text of a JSX element in a heading counts, and so does the code in
231
+ braces: `## Red <Badge>new</Badge> {version}` is `#/Red new version`. A heading inside a JSX element doesn't count.
232
+ A file that isn't valid MDX stops the run.
233
+
234
+ <!-- </block> -->
235
+
192
236
  ---
193
237
 
194
238
  [← Return to README](../README.md)
@@ -93,7 +93,8 @@ A separator must sit between two digits — `_1`, `1_`, `1__0` and `1_.0` are ty
93
93
  ## Notes
94
94
 
95
95
  - Blank lines and lines the pattern does not match are ignored, not treated as out of order. A line whose match is empty
96
- is ignored as well.
96
+ is ignored as well. So is a line that matches a branch without the `value` group: `^(?:none|(?P<value>\w+))$`
97
+ ignores the line `none`.
97
98
  - Comparison is on the trimmed line, so indentation does not affect ordering. A pattern is applied to the trimmed line
98
99
  too, so `^` and `$` anchor to the entry rather than to the indentation.
99
100
  - An unrecognized `keep-sorted` or `keep-sorted-format` value is a hard error, not a violation.
@@ -36,7 +36,8 @@ ids = [
36
36
  ```
37
37
 
38
38
  Without a `value` group the whole match is compared. Lines that do not match are skipped, as are
39
- lines whose match is empty.
39
+ lines whose match is empty. So is a line that matches a branch without the `value` group: `^(?:none|(?P<value>\w+))$`
40
+ skips the line `none`.
40
41
 
41
42
  ## Notes
42
43
 
@@ -56,8 +56,9 @@ Usually, though, the two places don't hold the same text. Then each block says *
56
56
  ## Pick values with `same-as-pattern`
57
57
 
58
58
  Each block has its own regex. Every match is a value: the `(?P<value>…)` group if there is one, otherwise the whole
59
- match. A line can hold several values. Lines without a match are skipped, and so are empty matches. This lets you
60
- compare blocks written in different formats:
59
+ match. A line can hold several values. Lines without a match are skipped, and so are empty matches. So is a match of a
60
+ branch without the `value` group: `^(?:none|(?P<value>\w+))$` skips the line `none`. This lets you compare blocks
61
+ written in different formats:
61
62
 
62
63
  ```rust
63
64
  // <block same-as="README.md:supported-env-vars" same-as-pattern="BLOCKWATCH_AI_[A-Z_]+">
@@ -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.11.3'],
9
+ install_requires=['blockwatch-cli==0.13.0'],
10
10
  # </block>
11
11
  # Without it, setuptools takes the directories under src/ for Python packages.
12
12
  packages=[],
@@ -206,6 +206,10 @@ impl<'source, I: Iterator<Item = Comment>> PartialBlocksIterator<'source, I> {
206
206
  fn take_adjacent_comment(&mut self, comment: &Comment) -> Option<Comment> {
207
207
  let contents = self.contents;
208
208
  self.comments.next_if(|next| {
209
+ #[expect(
210
+ clippy::string_slice,
211
+ reason = "parsers give comment ranges of whole characters"
212
+ )]
209
213
  contents[comment.source_range.end..next.source_range.start]
210
214
  .trim()
211
215
  .is_empty()
@@ -217,6 +221,10 @@ impl<'source, I: Iterator<Item = Comment>> PartialBlocksIterator<'source, I> {
217
221
  fn join_comments(comment: Comment, next: Comment, contents: &str) -> Comment {
218
222
  // The text between the comments is copied from the source, so an offset into the joined text
219
223
  // maps onto the source as it does for a single comment.
224
+ #[expect(
225
+ clippy::string_slice,
226
+ reason = "parsers give comment ranges of whole characters"
227
+ )]
220
228
  let between = &contents[comment.source_range.end..next.source_range.start];
221
229
  Comment {
222
230
  position_range: comment.position_range.start..next.position_range.end,
@@ -315,9 +323,14 @@ impl BlockStart {
315
323
  /// Columns count characters, so the offset is converted by counting the characters it skips
316
324
  /// rather than the bytes: the two differ on any line holding a multi-byte character.
317
325
  fn source_position_at(position_in_comment: usize, comment: &Comment) -> Position {
326
+ #[expect(
327
+ clippy::string_slice,
328
+ reason = "a tag starts at a `<` and ends after a `>`"
329
+ )]
318
330
  let text_before = &comment.comment_text[..position_in_comment];
319
331
  match text_before.rfind('\n') {
320
332
  // Past the comment's first line, so the column is measured from that line's start.
333
+ #[expect(clippy::string_slice, reason = "`line_break` is the index of a `\\n`")]
321
334
  Some(line_break) => Position::new(
322
335
  comment.position_range.start.line + text_before.matches('\n').count(),
323
336
  text_before[line_break + 1..].chars().count() + 1,
@@ -52,8 +52,9 @@ pub(crate) struct Content {
52
52
  /// The text of a block's [`Content`].
53
53
  #[derive(Debug, PartialEq, Eq, Clone)]
54
54
  enum ContentText {
55
- /// The source text in this byte range: the lines between the block's tags, or the definition
56
- /// of the object, list or table that a virtual block wraps.
55
+ /// The source text in this byte range: the lines between the block's tags, or the part of the
56
+ /// file that a virtual block wraps when it is written as is, such as a JSON object or a
57
+ /// Markdown section.
57
58
  Source(Range<usize>),
58
59
  /// The decoded value of the scalar that a virtual block wraps. Its quotes and escapes are
59
60
  /// gone, so it is not a part of the source text.
@@ -166,6 +167,10 @@ impl Content {
166
167
  /// Returns the content's text. `source` must be the text of the block's file.
167
168
  pub(crate) fn text<'a>(&'a self, source: &'a str) -> &'a str {
168
169
  match &self.text {
170
+ #[expect(
171
+ clippy::string_slice,
172
+ reason = "a comment, a symbol or a match sets `range`"
173
+ )]
169
174
  ContentText::Source(range) => &source[range.clone()],
170
175
  ContentText::Decoded(text) => text,
171
176
  }
@@ -1002,6 +1007,7 @@ pub(crate) fn parser_for_file_path<'p>(
1002
1007
  let file_name = file_path.file_name()?.to_str()?;
1003
1008
 
1004
1009
  for (i, _) in file_name.match_indices('.').rev() {
1010
+ #[expect(clippy::string_slice, reason = "`i` is the index of a `.`")]
1005
1011
  let extension = &file_name[i + 1..];
1006
1012
  let ext_os = OsString::from(extension);
1007
1013
 
@@ -16,7 +16,7 @@ use crate::violation_address::ViolationAddress;
16
16
  use crate::virtual_blocks::VirtualBlock;
17
17
  use anyhow::Context;
18
18
  use clap::Parser;
19
- use std::collections::HashMap;
19
+ use std::collections::{BTreeMap, HashMap};
20
20
  use std::io::{IsTerminal, Read, Write};
21
21
  use std::path::PathBuf;
22
22
  use std::process::ExitCode;
@@ -147,7 +147,7 @@ fn run_validators(args: &flags::Args) -> anyhow::Result<ExitCode> {
147
147
  /// both leave one behind, and neither says anything about the code being checked.
148
148
  fn apply_suppressions(
149
149
  suppressed_addresses: &[ViolationAddress],
150
- violations: &mut HashMap<RepoPath, Vec<Violation>>,
150
+ violations: &mut BTreeMap<RepoPath, Vec<Violation>>,
151
151
  ) {
152
152
  if suppressed_addresses.is_empty() {
153
153
  return;
@@ -329,7 +329,7 @@ fn read_diff_from_stdin(
329
329
  /// Writes the violations to stderr in the requested format. Returns true if any violation has error
330
330
  /// severity.
331
331
  fn process_violations(
332
- violations: &HashMap<RepoPath, Vec<Violation>>,
332
+ violations: &BTreeMap<RepoPath, Vec<Violation>>,
333
333
  format: flags::OutputFormat,
334
334
  ) -> anyhow::Result<bool> {
335
335
  let has_error_severity = violations.values().flatten().any(|violation| {
@@ -348,9 +348,10 @@ fn process_violations(
348
348
  Ok(has_error_severity)
349
349
  }
350
350
 
351
- /// Writes the violations to stderr as one JSON object of diagnostics grouped by file.
352
- fn write_json_violations(violations: &HashMap<RepoPath, Vec<Violation>>) -> anyhow::Result<()> {
353
- let diagnostics: HashMap<&RepoPath, Vec<_>> = violations
351
+ /// Writes the violations to stderr as one JSON object of diagnostics grouped by file, with the
352
+ /// files sorted by path.
353
+ fn write_json_violations(violations: &BTreeMap<RepoPath, Vec<Violation>>) -> anyhow::Result<()> {
354
+ let diagnostics: BTreeMap<&RepoPath, Vec<_>> = violations
354
355
  .iter()
355
356
  .map(|(file_path, file_violations)| {
356
357
  let file_diagnostics = file_violations
@@ -364,7 +365,7 @@ fn write_json_violations(violations: &HashMap<RepoPath, Vec<Violation>>) -> anyh
364
365
  }
365
366
 
366
367
  /// Writes the violations to stderr as a SARIF log.
367
- fn write_sarif_violations(violations: &HashMap<RepoPath, Vec<Violation>>) -> anyhow::Result<()> {
368
+ fn write_sarif_violations(violations: &BTreeMap<RepoPath, Vec<Violation>>) -> anyhow::Result<()> {
368
369
  write_json(std::io::stderr(), &sarif::SarifLog::new(violations))
369
370
  }
370
371
 
@@ -377,13 +377,15 @@ fn parse_suppressions(content: &str, path: &Path) -> anyhow::Result<Vec<Violatio
377
377
  let mut addresses = Vec::new();
378
378
  for (line_idx, line) in content.lines().enumerate() {
379
379
  let trimmed = line.trim();
380
- if trimmed.len() <= SUPPRESS_TRAILER_PREFIX.len()
381
- || !trimmed[..SUPPRESS_TRAILER_PREFIX.len()]
382
- .eq_ignore_ascii_case(SUPPRESS_TRAILER_PREFIX)
380
+ // Bytes, because the prefix's length can fall inside a multi-byte character.
381
+ if trimmed.len() < SUPPRESS_TRAILER_PREFIX.len()
382
+ || !trimmed.as_bytes()[..SUPPRESS_TRAILER_PREFIX.len()]
383
+ .eq_ignore_ascii_case(SUPPRESS_TRAILER_PREFIX.as_bytes())
383
384
  {
384
385
  // Non-matching line.
385
386
  continue;
386
387
  }
388
+ #[expect(clippy::string_slice, reason = "the prefix just matched is ASCII")]
387
389
  let address_str = trimmed[SUPPRESS_TRAILER_PREFIX.len()..].trim();
388
390
  let address = ViolationAddress::parse(address_str).with_context(|| {
389
391
  format!(
@@ -600,6 +602,36 @@ mod tests {
600
602
  Ok(())
601
603
  }
602
604
 
605
+ #[test]
606
+ fn multibyte_character_across_prefix_length_is_ignored() -> anyhow::Result<()> {
607
+ // `é` is 2 bytes: 19 and 20. The trailer prefix is 20 bytes long.
608
+ let content = format!(
609
+ "{0}é\n\
610
+ {0}é and more text\n\
611
+ blockwatch-suppress: docs/cli.md:cli-docs:keep-sorted\n",
612
+ "a".repeat(19)
613
+ );
614
+
615
+ assert_eq!(
616
+ parse_suppressions(&content, Path::new("commit_msg.txt"))?
617
+ .iter()
618
+ .map(ToString::to_string)
619
+ .collect::<Vec<_>>(),
620
+ vec!["docs/cli.md:cli-docs:keep-sorted".to_string()]
621
+ );
622
+ Ok(())
623
+ }
624
+
625
+ #[test]
626
+ fn trailer_without_address_fails() {
627
+ let error = parse_suppressions("blockwatch-suppress:\n", Path::new("commit_msg.txt"))
628
+ .expect_err("a trailer without an address must be rejected");
629
+ assert!(
630
+ error.to_string().contains("invalid suppression address"),
631
+ "unexpected error: {error}"
632
+ );
633
+ }
634
+
603
635
  #[test]
604
636
  fn suppress_from_with_nonexistent_file_fails_validation() -> anyhow::Result<()> {
605
637
  let args = parse(&["blockwatch", "--suppress-from", "nonexistent_file.txt"])?;
@@ -1,5 +1,6 @@
1
1
  use crate::language_parsers::{
2
2
  CommentsParser, LanguageParser, LanguageParserImpl, TreeSitterCommentsParser, comment_visitor,
3
+ node_text,
3
4
  };
4
5
 
5
6
  /// Returns a [`LanguageParser`] for Bash.
@@ -15,7 +16,7 @@ fn comments_parser() -> anyhow::Result<impl CommentsParser> {
15
16
  if node.kind() != "comment" {
16
17
  return None;
17
18
  }
18
- let comment = &source[node.byte_range()];
19
+ let comment = node_text(node, source);
19
20
  if comment.starts_with("#!") {
20
21
  // Skip shebang.
21
22
  None