@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.29 → 0.1.32

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 (201) hide show
  1. package/bin/adl +2 -2
  2. package/bin/health-check +2 -2
  3. package/bin/reconcile +41 -21
  4. package/bin/setup +2 -1
  5. package/package.json +1 -1
  6. package/packages/agent-hooks/README.md +77 -8
  7. package/packages/agent-hooks/assets/opencode-plugin.template.ts +2 -6
  8. package/packages/agent-hooks/src/cli/args.ts +2 -2
  9. package/packages/agent-hooks/src/cli/doctor.ts +2 -1
  10. package/packages/agent-hooks/src/cli/rules.ts +2 -1
  11. package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
  12. package/packages/agent-hooks/src/gating/config.ts +11 -0
  13. package/packages/agent-hooks/src/gating/running-adl.ts +24 -0
  14. package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
  15. package/packages/agent-hooks/src/hooks/log.ts +12 -12
  16. package/packages/agent-hooks/src/hooks/user-prompt-submit/handler.ts +2 -2
  17. package/packages/agent-hooks/src/log/writer.ts +170 -24
  18. package/packages/agent-hooks/src/main.ts +11 -4
  19. package/packages/agent-hooks/src/pre-tool-use/handler.ts +5 -4
  20. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  21. package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
  22. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
  23. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
  24. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
  25. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
  26. package/packages/azdo/src/azdo-wit.ts +8 -2
  27. package/packages/azdo/src/mcp-server.ts +1 -1
  28. package/packages/fleet/manifest.json +2 -1
  29. package/packages/fleet/src/repo-maintain.ts +5 -2
  30. package/packages/repo-controls/AGENTS.md +7 -4
  31. package/packages/repo-controls/README.md +346 -56
  32. package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
  33. package/packages/repo-controls/assets/promote.ts +7 -85
  34. package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
  35. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
  36. package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
  37. package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
  38. package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
  39. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
  40. package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
  41. package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
  42. package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
  43. package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
  44. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
  45. package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
  46. package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
  47. package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
  48. package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
  49. package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
  50. package/packages/repo-controls/assets/version-bump.sh +7 -48
  51. package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
  52. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
  53. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
  54. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
  55. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
  56. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/excluded/web/i18n/locales/zh-CN/strings.ts +1 -0
  57. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
  58. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
  59. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
  60. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
  61. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
  62. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
  63. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
  72. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
  73. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
  74. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
  75. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
  76. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/web/src/renderer/solo/item.ts +1 -0
  77. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/a.ts +1 -0
  78. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/b.ts +1 -0
  79. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
  80. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/fresh/fresh.ts +1 -0
  81. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub1/only.ts +1 -0
  82. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub10/only.ts +1 -0
  83. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub11/only.ts +1 -0
  84. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub2/only.ts +1 -0
  85. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub3/only.ts +1 -0
  86. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub4/only.ts +1 -0
  87. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub5/only.ts +1 -0
  88. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub6/only.ts +1 -0
  89. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub7/only.ts +1 -0
  90. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub8/only.ts +1 -0
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub9/only.ts +1 -0
  92. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/services/i18n/locales/de/common.json +1 -0
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/solo/item.ts +1 -0
  94. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/vendored-crate/Cargo.toml +2 -0
  95. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
  96. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/web/src/renderer/solo/item.ts +1 -0
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
  98. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/a.ts +1 -0
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/b.ts +1 -0
  100. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/c.ts +1 -0
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
  102. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/web/src/renderer/solo/item.ts +1 -0
  103. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
  104. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/fresh/fresh.ts +1 -0
  105. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub1/only.ts +1 -0
  106. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub10/only.ts +1 -0
  107. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub11/only.ts +1 -0
  108. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub2/only.ts +1 -0
  109. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub3/only.ts +1 -0
  110. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub4/only.ts +1 -0
  111. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub5/only.ts +1 -0
  112. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub6/only.ts +1 -0
  113. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub7/only.ts +1 -0
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub8/only.ts +1 -0
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub9/only.ts +1 -0
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/solo/item.ts +1 -0
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/web/src/renderer/solo/item.ts +1 -0
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
  124. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
  125. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
  126. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
  127. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
  128. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/route-target.md +8 -0
  129. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
  130. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/route-target.md +8 -0
  131. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
  132. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
  133. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
  134. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
  135. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
  136. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
  137. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
  138. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
  139. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
  140. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
  141. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
  142. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
  143. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
  144. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
  145. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
  146. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
  147. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
  148. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
  149. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
  150. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
  151. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
  152. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
  153. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
  154. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
  155. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
  156. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
  157. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
  158. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
  159. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
  160. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
  161. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
  162. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
  163. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
  164. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
  179. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
  180. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
  181. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
  182. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
  183. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
  184. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
  185. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  186. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
  187. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
  188. package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
  189. package/packages/repo-controls/scripts/hooks/pre-push +38 -20
  190. package/packages/repo-controls/src/cli.ts +22 -3
  191. package/packages/repo-controls/src/compliance-config.ts +132 -65
  192. package/packages/repo-controls/src/config.ts +55 -46
  193. package/packages/repo-controls/src/constants.ts +9 -0
  194. package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
  195. package/packages/repo-controls/src/doctor.ts +20 -30
  196. package/packages/repo-controls/src/findings-ratchet.ts +14 -0
  197. package/packages/repo-controls/src/gitignore-block.ts +3 -13
  198. package/packages/repo-controls/src/hook-log.ts +7 -10
  199. package/packages/repo-controls/src/hook.ts +127 -83
  200. package/packages/repo-controls/src/install-compliance.ts +112 -0
  201. package/packages/repo-controls/src/install.ts +133 -30
@@ -0,0 +1,101 @@
1
+ """REP-002 grades the repository-root manifests by itself (PBI 598)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from pathlib import Path
7
+
8
+ from adl_compliance import runner
9
+ from adl_compliance.core.verdict import Outcome, Verdict
10
+ from adl_compliance.policy.registry import load
11
+
12
+ ROW = load().by_id("REP-002")
13
+
14
+
15
+ def _write(root: Path, files: Mapping[str, str]) -> tuple[str, ...]:
16
+ for relative, body in files.items():
17
+ target = root / relative
18
+ target.parent.mkdir(parents=True, exist_ok=True)
19
+ target.write_text(body, encoding="utf-8")
20
+ return tuple(sorted(files))
21
+
22
+
23
+ def _grade(root: Path, files: tuple[str, ...], declarations: Mapping[str, str] | None = None) -> Verdict:
24
+ return runner.run(
25
+ ROW, root, files, exhaustive=True, universe=files,
26
+ declarations=dict(declarations or {}), trees={}, declared_trees={},
27
+ )
28
+
29
+
30
+ def test_a_root_cargo_manifest_without_a_lockfile_fails(tmp_path: Path) -> None:
31
+ files = _write(tmp_path, {"Cargo.toml": '[package]\nname = "x"\nversion = "0.1.0"\n'})
32
+ verdict = _grade(tmp_path, files)
33
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
34
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LCK-001", "Cargo.lock")]
35
+
36
+
37
+ def test_a_root_package_json_with_a_bun_lock_passes(tmp_path: Path) -> None:
38
+ files = _write(tmp_path, {"package.json": "{}\n", "bun.lock": "lock\n"})
39
+ verdict = _grade(tmp_path, files)
40
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
41
+
42
+
43
+ def test_a_root_package_json_without_a_bun_lock_fails(tmp_path: Path) -> None:
44
+ files = _write(tmp_path, {"package.json": "{}\n"})
45
+ verdict = _grade(tmp_path, files)
46
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LCK-001", "bun.lock")], verdict.render()
47
+
48
+
49
+ def test_an_empty_root_bun_lock_fails(tmp_path: Path) -> None:
50
+ files = _write(tmp_path, {"package.json": "{}\n", "bun.lock": ""})
51
+ verdict = _grade(tmp_path, files)
52
+ assert [f.path for f in verdict.findings] == ["bun.lock"], verdict.render()
53
+
54
+
55
+ def test_a_declared_lockfile_elsewhere_is_graded(tmp_path: Path) -> None:
56
+ declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
57
+ files = _write(tmp_path, {"apps/web/package.json": "{}\n"})
58
+ verdict = _grade(tmp_path, files, declarations)
59
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LCK-001", "apps/web/bun.lock")], verdict.render()
60
+
61
+
62
+ def test_a_declared_lockfile_that_is_present_passes(tmp_path: Path) -> None:
63
+ declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
64
+ files = _write(tmp_path, {"apps/web/package.json": "{}\n", "apps/web/bun.lock": "lock\n"})
65
+ verdict = _grade(tmp_path, files, declarations)
66
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
67
+
68
+
69
+ def test_a_nested_manifest_is_not_graded_unless_declared(tmp_path: Path) -> None:
70
+ """REWORKED, Tarun 2026-10-08: nothing to grade is a pass, not an error."""
71
+ files = _write(tmp_path, {"apps/web/package.json": "{}\n", "README.md": "x\n"})
72
+ verdict = _grade(tmp_path, files)
73
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
74
+
75
+
76
+ def test_the_root_pair_is_graded_beside_a_declaration(tmp_path: Path) -> None:
77
+ """Declaring another lockfile adds to the root pair; it does not replace it."""
78
+ declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
79
+ files = _write(tmp_path, {"package.json": "{}\n", "apps/web/package.json": "{}\n", "apps/web/bun.lock": "l\n"})
80
+ verdict = _grade(tmp_path, files, declarations)
81
+ assert [f.path for f in verdict.findings] == ["bun.lock"], verdict.render()
82
+
83
+
84
+ def test_no_root_manifest_and_no_declaration_is_clean(tmp_path: Path) -> None:
85
+ """REWORKED, Tarun 2026-10-08: a repository with no manifest has nothing to lock."""
86
+ files = _write(tmp_path, {"README.md": "x\n"})
87
+ verdict = _grade(tmp_path, files)
88
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
89
+
90
+
91
+ def test_a_lockfile_without_a_manifest_and_no_declaration_is_clean(tmp_path: Path) -> None:
92
+ files = _write(tmp_path, {"bun.lock": "lock\n"})
93
+ verdict = _grade(tmp_path, files)
94
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
95
+
96
+
97
+ def test_a_declared_manifest_matching_nothing_is_still_could_not_run(tmp_path: Path) -> None:
98
+ declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
99
+ files = _write(tmp_path, {"README.md": "x\n"})
100
+ verdict = _grade(tmp_path, files, declarations)
101
+ assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
@@ -36,7 +36,6 @@ from types import SimpleNamespace
36
36
  from typing import NamedTuple
37
37
 
38
38
  import pytest
39
- from fixture_trees import fixture_trees
40
39
  from helpers import corpus_files
41
40
 
42
41
  from adl_compliance import runner
@@ -293,7 +292,7 @@ SHAPES = [
293
292
 
294
293
 
295
294
  def lay(root: Path, files: Mapping[str, str], lock: str | bytes, *, where: str = "backend") -> None:
296
- """Write a cargo workspace under `root/where` plus the TypeScript lock the gate also wants.
295
+ """Write a cargo workspace under `root/where`.
297
296
 
298
297
  A manifest with a `[package]` gets a `src/lib.rs`, so the very same files are also valid
299
298
  input to the real `cargo metadata`. `where` is the cargo workspace directory.
@@ -311,15 +310,16 @@ def lay(root: Path, files: Mapping[str, str], lock: str | bytes, *, where: str =
311
310
  lockfile.write_bytes(lock)
312
311
  else:
313
312
  lockfile.write_text(lock, encoding="utf-8")
314
- (root / "frontend").mkdir(exist_ok=True)
315
- (root / "frontend" / "bun.lock").write_text("# fixture lock\n", encoding="utf-8")
316
313
 
317
314
 
318
- def grade(
319
- root: Path, *, declarations: Mapping[str, str] | None = None, trees: Mapping[str, str] | None = None
320
- ) -> Verdict:
315
+ #: Most tests lay their workspace under `backend/`; `grade` declares it unless a test passes its own.
316
+ BACKEND = {"cargo-manifests": "backend/Cargo.toml", "cargo-lockfiles": "backend/Cargo.lock"}
317
+
318
+
319
+ def grade(root: Path, *, declarations: Mapping[str, str] | None = None) -> Verdict:
321
320
  """The real dispatch path, whole tree, exhaustive."""
322
- trees = fixture_trees() if trees is None else trees
321
+ declarations = BACKEND if declarations is None else declarations
322
+ trees: dict[str, str] = {}
323
323
  files = corpus_files(root)
324
324
  return runner.run(
325
325
  ROW,
@@ -327,7 +327,7 @@ def grade(
327
327
  files,
328
328
  exhaustive=True,
329
329
  universe=files,
330
- declarations=declarations or {},
330
+ declarations=declarations,
331
331
  trees=trees,
332
332
  declared_trees=trees,
333
333
  )
@@ -1071,12 +1071,24 @@ def test_a_cargo_rejection_is_the_only_finding_for_that_lock(
1071
1071
  assert [argv[:2] for argv in calls] == [["cargo", "--version"], ["cargo", "metadata"]]
1072
1072
 
1073
1073
 
1074
- def test_the_undeclared_mode_grades_the_tree_default_lockfile(
1074
+ def test_the_root_pair_is_graded_without_any_declaration(
1075
1075
  tmp_path: Path, cargo_accepts: list[list[str]]
1076
1076
  ) -> None:
1077
- verdict = stale_lock(tmp_path, SINGLE.files, "version = 99\n")
1078
- assert {f.path for f in verdict.findings} == {"backend/Cargo.lock"}
1079
- assert {f.fix for f in verdict.findings} == {FIX}
1077
+ lay(tmp_path, SINGLE.files, "version = 99\n", where=".")
1078
+ verdict = grade(tmp_path, declarations={})
1079
+ assert {f.path for f in verdict.findings} == {"Cargo.lock"}
1080
+ assert {f.fix for f in verdict.findings} == {
1081
+ "run: (cd . && cargo update) to resync Cargo.lock with Cargo.toml, then commit the result"
1082
+ }
1083
+
1084
+
1085
+ def test_a_workspace_below_the_root_is_not_graded_unless_declared(
1086
+ tmp_path: Path, cargo_accepts: list[list[str]]
1087
+ ) -> None:
1088
+ lay(tmp_path, SINGLE.files, "version = 99\n")
1089
+ verdict = grade(tmp_path, declarations={})
1090
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
1091
+ assert cargo_accepts == []
1080
1092
 
1081
1093
 
1082
1094
  DECLARED = {"cargo-manifests": "nested/*/Cargo.toml", "cargo-lockfiles": "nested/ws/Cargo.lock"}
@@ -0,0 +1,7 @@
1
+ """The `repository.no-multiline-comments` gate (REP-007)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .check import NoMultilineComments
6
+
7
+ __all__ = ["NoMultilineComments"]
@@ -0,0 +1,124 @@
1
+ """REP-007 (MLC-001, MLC-002): no multi-line comment in code (Bug 602)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import ClassVar, Literal
7
+
8
+ import yaml
9
+ from pydantic import field_validator
10
+
11
+ from ....core.check import Check
12
+ from ....core.context import Context, FileSet
13
+ from ....core.errors import CouldNotRun, SchemaInvalid
14
+ from ....core.finding import Report
15
+ from ....core.models import StrictModel
16
+ from ...lfs.integrity.check import UniqueKeyLoader
17
+ from . import scanner
18
+
19
+
20
+ class Register(StrictModel):
21
+ """The repository's recorded debt: the files that already carry one."""
22
+
23
+ schema_version: Literal[1]
24
+ files: list[str]
25
+
26
+ @field_validator("files")
27
+ @classmethod
28
+ def _paths_are_normalised_and_unique(cls, value: list[str]) -> list[str]:
29
+ for path in value:
30
+ if "\\" in path or any(part in ("", ".", "..") for part in path.split("/")):
31
+ raise ValueError(f"{path!r} is not a normalised repository-relative file path")
32
+ if len(set(value)) != len(value):
33
+ raise ValueError("a path is listed more than once")
34
+ return value
35
+
36
+
37
+ class NoMultilineComments(Check):
38
+ id: ClassVar[str] = "REP-007"
39
+ rule: ClassVar[str] = "MLC-001"
40
+ STALE: ClassVar[str] = "MLC-002"
41
+ UNPARSEABLE: ClassVar[str] = "MLC-003"
42
+
43
+ requires: ClassVar[tuple[str, ...]] = ("ast-grep", "shfmt")
44
+
45
+ FIX: ClassVar[str] = (
46
+ "move the explanation to docs/ and leave at most a one-line pointer"
47
+ )
48
+
49
+ inputs: ClassVar[dict[str, str]] = {
50
+ "register": "<compliance>/ratchets/REP-007-multiline-comments.yaml",
51
+ }
52
+ widening_inputs: ClassVar[frozenset[str]] = frozenset({"register"})
53
+
54
+ def structural(self, ctx: Context) -> Iterable[Report]:
55
+ for rel in sorted(self._register(ctx)):
56
+ why = self._why_stale(ctx, rel)
57
+ if why is not None:
58
+ yield self.report(
59
+ rel,
60
+ f"ratchet entry for {rel} is no longer required: {why}",
61
+ f"remove it from the register: the ratchet only shrinks "
62
+ f"(delete {rel} from files in {self._shown(ctx)})",
63
+ rule=self.STALE,
64
+ )
65
+
66
+ def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
67
+ granted = set(self._register(ctx))
68
+ for rel in files:
69
+ if rel in granted or not scanner.is_code_file(rel, lambda rel=rel: self.text(ctx, rel)):
70
+ continue
71
+ try:
72
+ hits = self._hits(ctx, rel)
73
+ except scanner.Unparseable as exc:
74
+ yield self.report(
75
+ rel, str(exc), "fix the file so it parses, or register it in the REP-007 ratchet",
76
+ rule=self.UNPARSEABLE,
77
+ )
78
+ continue
79
+ for line, what in hits:
80
+ yield self.report(
81
+ rel,
82
+ f"multi-line comment ({what})",
83
+ self.FIX,
84
+ line=line,
85
+ )
86
+
87
+ def _hits(self, ctx: Context, rel: str) -> list[tuple[int, str]]:
88
+ body = self.text(ctx, rel)
89
+ return [] if body is None else scanner.find(rel, body)
90
+
91
+ def _why_stale(self, ctx: Context, rel: str) -> str | None:
92
+ if not ctx.path(rel).is_file():
93
+ return "it no longer exists"
94
+ if not scanner.is_code_file(rel, lambda: self.text(ctx, rel)):
95
+ return "it is not a code file this gate grades"
96
+ try:
97
+ hits = self._hits(ctx, rel)
98
+ except scanner.Unparseable:
99
+ return None
100
+ if not hits:
101
+ return "it no longer has a multi-line comment"
102
+ return None
103
+
104
+ # ------------------------------------------------------------ the register
105
+
106
+ def _register(self, ctx: Context) -> tuple[str, ...]:
107
+ """The files granted, or `()` when the repository records none."""
108
+ target = ctx.inputs.get("register")
109
+ if target is None or not target.exists():
110
+ return ()
111
+ shown = self._shown(ctx)
112
+ if not target.is_file():
113
+ raise CouldNotRun(f"REP-007 ratchet register {shown} exists but is not a file")
114
+ body = self.text(ctx, target)
115
+ try:
116
+ loaded = yaml.load(body, Loader=UniqueKeyLoader) # noqa: S506 - a SafeLoader
117
+ return tuple(self._validate(loaded, Register, shown).files)
118
+ except yaml.YAMLError as exc:
119
+ raise CouldNotRun(f"REP-007 ratchet register {shown} is not valid YAML: {exc}") from exc
120
+ except SchemaInvalid as exc:
121
+ raise CouldNotRun(f"REP-007 ratchet register is unusable: {exc.detail}") from exc
122
+
123
+ def _shown(self, ctx: Context) -> str:
124
+ return ctx.relative(ctx.inputs["register"])
@@ -0,0 +1,4 @@
1
+ schema_version: 1
2
+ files:
3
+ - packages/app/src/legacy.ts
4
+ - scripts/old.sh
@@ -0,0 +1,217 @@
1
+ """Where multi-line comments are, per language: python by tokenize and ast, shell by shfmt, the rest by ast-grep."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import io
7
+ import json
8
+ import re
9
+ import subprocess
10
+ import tokenize
11
+ from dataclasses import dataclass
12
+
13
+ from ....core.errors import CouldNotRun
14
+ from ....core.scripts import is_code_file, language
15
+
16
+ __all__ = ["Unparseable", "find", "is_code_file"]
17
+
18
+
19
+ class Unparseable(Exception):
20
+ """The file cannot be parsed by the tool that reads its language: a finding about that file, not a failed gate."""
21
+
22
+ def __init__(self, relative: str, tool: str, detail: str) -> None:
23
+ super().__init__(f"{relative} cannot be parsed: {tool} {detail}")
24
+
25
+
26
+ class Syntax:
27
+ """The scanner's literals, on a class like every gate's."""
28
+
29
+ DIRECTIVE = re.compile(
30
+ r"(?:@ts-expect-error|@ts-ignore|@ts-nocheck"
31
+ r"|eslint-(?:disable|enable)(?:-next-line|-line)?(?:\s+[\w@/.-]+(?:\s*,\s*[\w@/.-]+)*)?"
32
+ r"|noqa(?::\s*[A-Z]+[0-9]+(?:[,\s]+[A-Z]+[0-9]+)*)?"
33
+ r"|type:\s*ignore(?:\[[\w, -]+\])?"
34
+ r"|biome-ignore(?:-all|-start|-end)?(?:\s+[\w/.-]+)*:?"
35
+ r"|shellcheck\s+(?:disable|enable|source|shell)=\S+"
36
+ r"|pyright:\s*(?:ignore|basic|strict|standard|off|report\w+)(?:\[[\w, -]+\])?(?:\s*=\s*\w+)?)"
37
+ r"\s*"
38
+ )
39
+
40
+ GRAMMARS = {"rust": "Rust", "slash": "Tsx", "typescript": "TypeScript", "justfile": "Bash"}
41
+ SHFMT_DIALECTS = {"shell": "bash", "posix": "posix", "mksh": "mksh", "zsh": "zsh"}
42
+ BRACES = "{}"
43
+ JSX_LANGUAGES = frozenset({"slash"})
44
+ RUST_RULE = "any:\n - kind: line_comment\n - kind: block_comment"
45
+ COMMENT_RULE = "kind: comment"
46
+ SG_TIMEOUT = 60
47
+
48
+
49
+ def _is_directive(body: str) -> bool:
50
+ """A comment that is solely a tool directive; prose after it makes it prose."""
51
+ return Syntax.DIRECTIVE.fullmatch(body.strip()) is not None
52
+
53
+
54
+ def find(relative: str, text: str) -> list[tuple[int, str]]:
55
+ """Every multi-line comment in `text`, ordered by line."""
56
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
57
+ kind = language(relative, text)
58
+ if kind == "python":
59
+ hits = _python(relative, text)
60
+ elif kind is not None:
61
+ hits = _tree(relative, text, kind)
62
+ else:
63
+ hits = []
64
+ return sorted(hits)
65
+
66
+
67
+ def _runs(lines: dict[int, bool], what: str) -> list[tuple[int, str]]:
68
+ """Runs of consecutive lines with two or more prose lines (a directive-only line is transparent), at the first."""
69
+ hits: list[tuple[int, str]] = []
70
+ run: list[int] = []
71
+ for line in [*sorted(lines), -1]:
72
+ if run and line == run[-1] + 1:
73
+ run.append(line)
74
+ continue
75
+ if sum(1 for n in run if not lines[n]) > 1:
76
+ hits.append((run[0], f"{len(run)} consecutive {what}"))
77
+ run = [line]
78
+ return hits
79
+
80
+
81
+ # ------------------------------------------------------------------- python
82
+ def _python(relative: str, text: str) -> list[tuple[int, str]]:
83
+ try:
84
+ tree = ast.parse(text)
85
+ tokens = list(tokenize.generate_tokens(io.StringIO(text).readline))
86
+ except (SyntaxError, ValueError, tokenize.TokenError) as exc:
87
+ raise Unparseable(relative, "python", str(exc)) from exc
88
+ comment_lines: dict[int, bool] = {}
89
+ for token in tokens:
90
+ if token.type != tokenize.COMMENT:
91
+ continue
92
+ row, col = token.start
93
+ if token.line[:col].strip():
94
+ continue # trailing
95
+ if row == 1 and token.string.startswith("#!"):
96
+ continue
97
+ comment_lines[row] = _is_directive(token.string.lstrip("#"))
98
+ hits = _runs(comment_lines, "line comments")
99
+ single: dict[int, bool] = {}
100
+ for node in ast.walk(tree):
101
+ if not (isinstance(node, ast.Expr) and node.end_lineno is not None and _is_string(node.value)):
102
+ continue
103
+ if node.end_lineno > node.lineno:
104
+ hits.append((node.lineno, f"string statement (docstring) spanning {node.end_lineno - node.lineno + 1} lines"))
105
+ else:
106
+ single[node.lineno] = False
107
+ return hits + _runs(single, "string statements")
108
+
109
+
110
+ def _is_string(node: ast.expr) -> bool:
111
+ return isinstance(node, ast.JoinedStr) or (
112
+ isinstance(node, ast.Constant) and isinstance(node.value, (str, bytes))
113
+ )
114
+
115
+
116
+ # ------------------------------------------------------- every other language
117
+ @dataclass(frozen=True)
118
+ class _Comment:
119
+ """One comment node: its 1-based first and last line, the text, and its start column."""
120
+
121
+ line: int
122
+ end: int
123
+ text: str
124
+ column: int
125
+
126
+
127
+ def _tree(relative: str, text: str, language: str) -> list[tuple[int, str]]:
128
+ lines = text.split("\n")
129
+ hits: list[tuple[int, str]] = []
130
+ per_line: dict[int, list[_Comment]] = {}
131
+ found = _shell_comments(relative, text, language) if language in Syntax.SHFMT_DIALECTS else _comments(
132
+ relative, text, Syntax.GRAMMARS[language]
133
+ )
134
+ for comment in found:
135
+ if comment.line == 1 and comment.text.startswith("#!"):
136
+ continue
137
+ if comment.end > comment.line:
138
+ hits.append((comment.line, f"block comment spanning {comment.end - comment.line + 1} lines"))
139
+ else:
140
+ per_line.setdefault(comment.line, []).append(comment)
141
+ standalone: dict[int, bool] = {}
142
+ for number, comments in per_line.items():
143
+ rest = lines[number - 1]
144
+ for comment in sorted(comments, key=lambda c: -c.column):
145
+ rest = rest[: comment.column] + rest[comment.column + len(comment.text) :]
146
+ if "".join(rest.split()) != "" and not _jsx_comment_line(lines[number - 1], comments, language):
147
+ continue # trailing
148
+ standalone[number] = all(_is_directive(_body(c.text)) for c in comments)
149
+ return hits + _runs(standalone, "line comments")
150
+
151
+
152
+ def _jsx_comment_line(line: str, comments: list[_Comment], language: str) -> bool:
153
+ """A JSX comment line: `{`, one block comment, `}` and nothing else, in a language that has JSX."""
154
+ if language not in Syntax.JSX_LANGUAGES or len(comments) != 1 or not comments[0].text.startswith("/*"):
155
+ return False
156
+ comment = comments[0]
157
+ before, after = line[: comment.column], line[comment.column + len(comment.text) :]
158
+ return before.strip() == "{" and after.strip() == "}"
159
+
160
+
161
+ def _body(text: str) -> str:
162
+ """A comment without its delimiters."""
163
+ if text.startswith("/*"):
164
+ return text[2:-2].lstrip("*!")
165
+ return text.lstrip("/#!")
166
+
167
+
168
+ def _comments(relative: str, text: str, grammar: str) -> list[_Comment]:
169
+ rule = Syntax.RUST_RULE if grammar == "Rust" else Syntax.COMMENT_RULE
170
+ inline = f"id: comments\nseverity: hint\nlanguage: {grammar}\nrule:\n {rule}\n"
171
+ try:
172
+ done = subprocess.run(
173
+ ["ast-grep", "scan", "--stdin", "--inline-rules", inline, "--json=stream"],
174
+ input=text, capture_output=True, text=True, check=False, timeout=Syntax.SG_TIMEOUT,
175
+ )
176
+ except (OSError, subprocess.TimeoutExpired) as exc:
177
+ raise CouldNotRun(f"{relative} cannot be parsed: ast-grep: {exc}") from exc
178
+ if done.returncode != 0:
179
+ raise Unparseable(relative, "ast-grep", done.stderr.strip() or f"exited {done.returncode}")
180
+ found: list[_Comment] = []
181
+ for record in done.stdout.splitlines():
182
+ if not record.strip():
183
+ continue
184
+ node = json.loads(record)
185
+ body = node["text"].rstrip("\n")
186
+ start = node["range"]["start"]
187
+ found.append(_Comment(start["line"] + 1, start["line"] + 1 + body.count("\n"), body, start["column"]))
188
+ return found
189
+
190
+
191
+ def _shell_comments(relative: str, text: str, language: str) -> list[_Comment]:
192
+ command = ["shfmt", "--tojson", "--language-dialect", Syntax.SHFMT_DIALECTS[language]]
193
+ try:
194
+ done = subprocess.run(
195
+ command, input=text, capture_output=True, text=True, check=False, timeout=Syntax.SG_TIMEOUT
196
+ )
197
+ except (OSError, subprocess.TimeoutExpired) as exc:
198
+ raise CouldNotRun(f"{relative} cannot be parsed: shfmt: {exc}") from exc
199
+ if done.returncode != 0:
200
+ raise Unparseable(relative, "shfmt", done.stderr.strip() or f"exited {done.returncode}")
201
+ found: dict[tuple[int, int], _Comment] = {}
202
+ lines = text.split("\n")
203
+ for node in _comment_nodes(json.loads(done.stdout)):
204
+ line, column = node["Pos"]["Line"], node["Pos"]["Col"] - 1
205
+ body = "#" + node["Text"]
206
+ found[(line, column)] = _Comment(line, line, body, len(lines[line - 1].encode()[:column].decode(errors="ignore")))
207
+ return list(found.values())
208
+
209
+
210
+ def _comment_nodes(tree: object) -> list[dict]:
211
+ """Every comment node in a shfmt syntax tree: the objects carrying a `Hash` and a `Text`."""
212
+ if isinstance(tree, dict):
213
+ own = [tree] if "Hash" in tree and "Text" in tree else []
214
+ return own + [n for value in tree.values() for n in _comment_nodes(value)]
215
+ if isinstance(tree, list):
216
+ return [n for value in tree for n in _comment_nodes(value)]
217
+ return []