@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.30 → 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 (192) 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/deploy/opencode-template.ts +5 -3
  9. package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
  10. package/packages/agent-hooks/src/hooks/log.ts +12 -12
  11. package/packages/agent-hooks/src/log/writer.ts +170 -24
  12. package/packages/agent-hooks/src/main.ts +1 -1
  13. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  14. package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
  15. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
  16. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
  17. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
  18. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
  19. package/packages/fleet/manifest.json +2 -1
  20. package/packages/fleet/src/repo-maintain.ts +5 -2
  21. package/packages/repo-controls/AGENTS.md +7 -4
  22. package/packages/repo-controls/README.md +346 -56
  23. package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
  24. package/packages/repo-controls/assets/promote.ts +7 -85
  25. package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
  26. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
  27. package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
  28. package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
  29. package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
  30. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
  31. package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
  32. package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
  33. package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
  34. package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
  35. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
  36. package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
  37. package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
  38. package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
  39. package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
  40. package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
  41. package/packages/repo-controls/assets/version-bump.sh +7 -48
  42. package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
  43. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
  44. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
  45. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
  46. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
  47. 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
  48. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
  49. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
  50. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
  51. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
  52. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
  53. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
  54. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
  55. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
  56. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
  57. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
  58. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
  59. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
  60. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
  61. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
  62. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
  63. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
  67. 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
  68. 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
  69. 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
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
  87. 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
  88. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
  89. 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
  90. 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
  91. 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
  92. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
  93. 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
  94. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
  95. 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
  96. 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
  97. 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
  98. 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
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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
  106. 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
  107. 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
  108. 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
  109. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
  110. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
  111. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
  112. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
  113. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
  119. 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
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
  121. 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
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
  124. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
  125. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
  126. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
  127. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
  128. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
  129. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
  130. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
  131. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
  132. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
  133. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
  134. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
  135. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
  136. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
  137. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
  138. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
  139. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
  140. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
  141. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
  142. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
  143. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
  144. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
  145. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
  146. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
  147. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
  148. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
  149. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
  150. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
  151. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
  152. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
  153. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
  154. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
  155. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
  156. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
  157. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
  158. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
  159. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
  160. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
  161. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
  162. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
  163. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
  164. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
  179. package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
  180. package/packages/repo-controls/scripts/hooks/pre-push +27 -19
  181. package/packages/repo-controls/src/cli.ts +22 -3
  182. package/packages/repo-controls/src/compliance-config.ts +132 -65
  183. package/packages/repo-controls/src/config.ts +55 -46
  184. package/packages/repo-controls/src/constants.ts +9 -0
  185. package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
  186. package/packages/repo-controls/src/doctor.ts +20 -30
  187. package/packages/repo-controls/src/findings-ratchet.ts +14 -0
  188. package/packages/repo-controls/src/gitignore-block.ts +3 -13
  189. package/packages/repo-controls/src/hook-log.ts +7 -10
  190. package/packages/repo-controls/src/hook.ts +127 -83
  191. package/packages/repo-controls/src/install-compliance.ts +112 -0
  192. package/packages/repo-controls/src/install.ts +133 -30
@@ -3,8 +3,8 @@
3
3
  LAY-006 a Markdown file outside the declared docs trees and not on the
4
4
  exclusion list.
5
5
 
6
- The allowed roots are RESOLVED, not listed: `<docs>`, `<plans>`, `<compliance>`,
7
- `<rust>`, `<frontend>` and `<workflows>` come from
6
+ The allowed roots are RESOLVED, not listed: `<docs>`, `<plans>`, `<compliance>`
7
+ and `<workflows>` come from
8
8
  `[controls.compliance.trees]`, so a repository that spells a tree differently is
9
9
  graded against its own spelling. The only literals left are `*/LEDGER.md` (a
10
10
  register beside the tree it records) and the exclusion list below.
@@ -15,14 +15,6 @@ directory whose Markdown is site or application content rather than
15
15
  documentation. A declared root only ever WIDENS the allowlist, so declaring
16
16
  nothing is the stricter grade.
17
17
 
18
- This docstring used to read "docs/, .plans/, .specs/, or .changeset/" and three
19
- of those four were wrong. `.specs` was deleted 2026-09-16 as a path that matched
20
- nothing in this estate and never could; `.plans` became the resolvable `<plans>`
21
- when 7b8c057 renamed the tree; `.changeset/*.md` went the same way as `.specs`,
22
- for the same reason -- zero files across all four repositories. A rule's own
23
- description is the first thing anyone reads, and this one described the
24
- pre-migration world.
25
-
26
18
  LAY-006 a Markdown file outside the allowed docs trees and not on the
27
19
  exclusion list.
28
20
 
@@ -54,11 +46,11 @@ from ....policy.root_shape import (
54
46
  GOVERNANCE,
55
47
  GUIDES,
56
48
  REGISTERS,
57
- ROOT_FILES_DECLARATION,
58
49
  declared_root_names,
59
50
  )
60
51
  from ....core.check import Check
61
52
  from ....core.context import Context, FileSet
53
+ from ....core.declarations import declared_root_files
62
54
  from ....core.errors import CouldNotRun
63
55
  from ....core.finding import Report
64
56
 
@@ -83,16 +75,11 @@ class MarkdownTrees(Check):
83
75
  # Agent DEFINITIONS, same reasoning and same shape as the skill
84
76
  # definitions two lines up: `name`/`description`/`model`/`tools`
85
77
  # frontmatter, loaded by the runtime, not read as prose. Caught live
86
- # against enact-extensions: `extensions/plugin-dev/agents/{agent-
78
+ # against the extensions repository: `extensions/plugin-dev/agents/{agent-
87
79
  # creator,plugin-validator,skill-reviewer}.md` were reported as
88
80
  # documentation outside the allowed docs trees. COVERAGE COST, stated
89
- # not buried: 5 `agents/*.md` files across the four managed repos stop
90
- # being graded by this check (enact-extensions 3, enact-m5 2 — the
91
- # enact-m5 pair, `docs/architecture/agents/{mcp-servers,
92
- # orchestration}.md`, are prose docs already reachable via `docs/*.md`
93
- # in ALLOWED below, so this exclusion changes nothing about whether
94
- # they are reported, only widens what would also excuse them if they
95
- # ever moved outside `docs/`).
81
+ # not buried: `agents/*.md` files stop being graded by this check; prose
82
+ # docs among them are still reachable via `docs/*.md` in ALLOWED below.
96
83
  "agents/*", "*/agents/*",
97
84
  # Root governance and guides come from policy/root_shape.py, the single list
98
85
  # 63cd856 created when three gates were found carrying three drifting copies
@@ -104,14 +91,9 @@ class MarkdownTrees(Check):
104
91
  # `_excluded`.
105
92
  *sorted(GOVERNANCE | GUIDES | REGISTERS),
106
93
  "AGENTS.md", "CLAUDE.md", "CHANGELOG.md", "RELEASE.md",
107
- "INVARIANTS.md", "README.md", "CLAUDE-INVARIANTS.md",
108
- "INDEPENDENT-VERIFICATION.md",
94
+ "INVARIANTS.md", "README.md",
109
95
  "*/AGENTS.md", "*/CLAUDE.md", "*/SKILLS.md", "*/CHANGELOG.md",
110
96
  "*/README.md",
111
- # A tree's CLAUDE-INVARIANTS.md is the same class of control artifact
112
- # as its AGENTS.md and CLAUDE.md above; the root one is allowed two
113
- # patterns up and nowhere else.
114
- "*/CLAUDE-INVARIANTS.md",
115
97
  )
116
98
 
117
99
  # FIXED 2026-09-16 (K-42 sweep, LOUD form, approved): these five patterns
@@ -124,39 +106,19 @@ class MarkdownTrees(Check):
124
106
  # literal below its resolved root: named subject matter, not a resolvable
125
107
  # path.
126
108
  EXCLUDED_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
127
- ("frontend_root", ".claude/*"),
128
109
  ("workflows_root", "changesets/*"),
129
- # Two patterns, not three — see check.sh's comment: the nested form
130
- # needs its own line because the literal `tests/` anchors it, and
131
- # `tests/fixture*/*` already covers the zero-directory case.
132
- ("rust_root", "crates/*/tests/fixture*/*"),
133
- ("rust_root", "crates/*/tests/*/fixture*/*"),
134
- ("rust_root", "crates/enact-session/src/backend/codex_init_prompt.md"),
135
- ("frontend_root", "examples/*"),
136
- ("workflows_root", "COVERAGE.md"),
137
110
  )
138
111
 
139
112
  # Allowlisted Markdown roots.
140
113
  # `plans/`, not `.plans/`: 7b8c057 renamed the tree and `config.toml` now reads
141
114
  # `plans = "plans"`. This list did not follow, so every file in the tree the
142
115
  # registry points at was reported as being outside it -- 18 findings against
143
- # enact-m5 alone. The dotted spelling stays alongside it because a repository
116
+ # a managed repository alone. The dotted spelling stays alongside it because a repository
144
117
  # that has not migrated yet is not thereby wrong.
145
118
  # FIXED 2026-09-16 (K-42 sweep, LOUD form, approved): `docs/*.md`/
146
- # `docs/*.mdx`, `plans/*.md`, and `.specs/*.md` were also hardcoded
147
- # literals -- the current packaged default for each tree, spelled out
148
- # directly rather than resolved. `.plans/*.md` (legacy dotted spelling)
149
- # and bare `specs/*.md` (no documented migration away from `.specs`)
150
- # stay literal alternate-spelling allowances, exactly as before -- this
151
- # fix only moves the entry that stood for "the packaged default",
152
- # leaving the other spelling in each pair untouched.
119
+ # `docs/*.mdx` and `plans/*.md` were also hardcoded literals -- the packaged
120
+ # default for each tree, spelled out directly rather than resolved.
153
121
  ALLOWED: ClassVar[tuple[str, ...]] = (
154
- # `specs/*.md` deleted 2026-09-16. It was copy-paste from the
155
- # `plans/`/`.plans/` pair, which stood for a REAL rename. No repository
156
- # in this estate has `specs/` or `.specs/`, and nothing records an
157
- # undotted spelling as a rename target -- it matched nothing and never
158
- # could. The resolvable `<specs>` spelling is handled with the other
159
- # declared trees above.
160
122
  # A LEDGER beside the tree it records. `root_shape.REGISTERS` already
161
123
  # permits a register at the repository root "because that is where
162
124
  # someone arriving at the repository looks" -- a per-tree ledger is the
@@ -185,20 +147,11 @@ class MarkdownTrees(Check):
185
147
  "registers": "<compliance>",
186
148
  "docs_root": "<docs>",
187
149
  "plans_root": "<plans>",
188
- "rust_root": "<rust>",
189
- "frontend_root": "<frontend>",
190
150
  "workflows_root": "<workflows>",
191
151
  }
192
152
 
193
- # `<specs>` only ever ADDS allowlist entries here. enact-m5 has no specs
194
- # tree; requiring one would push the owner to declare a directory that does
195
- # not exist, and a false declaration is undetectable. Absent -> stricter.
196
- #
197
- # This input is reachable only by a caller that builds a Context by hand (the
198
- # unit tests do): `cli.py` routes global inputs to batch rows alone, so no
199
- # `adl-config.toml` key ever reached `specs_root`. A repository's own extra
200
- # Markdown roots arrive through the `markdown-roots` declaration below.
201
- widening_inputs: ClassVar[frozenset[str]] = frozenset({"specs_root"})
153
+ # `registers` only ever ADDS allowlist entries (PBI 599); no ratchet means the fixed list alone.
154
+ widening_inputs: ClassVar[frozenset[str]] = frozenset({"registers"})
202
155
 
203
156
  # `[controls.compliance.markdown].roots` -- directories a repository declares
204
157
  # as homes for Markdown it authors outside every tree above, e.g. an Astro
@@ -251,9 +204,6 @@ class MarkdownTrees(Check):
251
204
  plans_root = ctx.inputs.get("plans_root")
252
205
  if plans_root is not None:
253
206
  allowed = (*allowed, f"{ctx.relative(plans_root)}/*.md")
254
- specs_root = ctx.inputs.get("specs_root")
255
- if specs_root is not None:
256
- allowed = (*allowed, f"{ctx.relative(specs_root)}/*.md")
257
207
  # A declared root admits both suffixes this gate grades, like `docs_root`
258
208
  # -- admitting `.md` and reporting `.mdx` beside it would look like half
259
209
  # the declaration was ignored. Escaped: `_matches_any` is `fnmatchcase`, so
@@ -270,16 +220,12 @@ class MarkdownTrees(Check):
270
220
  root = ctx.inputs.get(input_name)
271
221
  if root is not None:
272
222
  excluded = (*excluded, f"{ctx.relative(root)}/{suffix}")
273
- # Root Markdown the repository declared under `[controls.compliance.root]
274
- # files` (the `root-files` declaration). Matched EXACTLY: `_matches_any`
275
- # is `fnmatchcase`, so a declared name carrying `*`, `?` or `[` must be
276
- # escaped or it would exclude more than it names. A root name has no `/`,
277
- # so a nested file of the same name is not the declared one.
223
+ # Root Markdown declared under `[controls.compliance.root] files`, matched exactly (no globs).
278
224
  declared = sorted(
279
- name for name in declared_root_names(ctx.declarations, ROOT_FILES_DECLARATION)
225
+ name for name in declared_root_files(ctx.declarations)
280
226
  if "/" not in name and name.endswith(self.MARKDOWN_SUFFIXES)
281
227
  )
282
- return (*excluded, *(glob.escape(name) for name in declared))
228
+ return (*excluded, *declared)
283
229
 
284
230
  def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
285
231
  allowed = self._allowed(ctx)
@@ -295,13 +241,7 @@ class MarkdownTrees(Check):
295
241
  yield self.report(
296
242
  relative,
297
243
  "documentation file outside the allowed docs trees",
298
- # The remedy names the trees THIS repository declared, resolved
299
- # the same way the rule was graded. It used to read
300
- # "docs/, plans/, .specs/, or .changeset/" -- two retired trees
301
- # and a hardcoded literal -- so a reader following it moved the
302
- # file somewhere no repository has, and the gate failed again.
303
- # A fix line that cannot be followed is worse than none: it
304
- # spends the reader's trust and returns nothing.
244
+ # The remedy names the trees THIS repository declared.
305
245
  f"move {relative} under one of this repository's declared doc "
306
246
  f"trees ({', '.join(declared) if declared else 'none declared'}), "
307
247
  "or add it to the exclusion list in this gate if it is a "
@@ -3,10 +3,10 @@
3
3
  Two corpora, committed byte-for-byte from the shell tree's fixtures:
4
4
  `markdown-trees-stray/` puts one Markdown page outside every allowed tree
5
5
  beside one that IS allowed, so the gate must report exactly the first;
6
- `markdown-trees-clean/` holds four files, each excluded or allowed for a
7
- DIFFERENT reason — an allowed tree, a second allowed tree, a control artifact
8
- (`backend/AGENTS.md`), and a crate test fixture — so that a gate reporting
9
- every `.md` it sees, or one exclusion family rotting silently, would fail it.
6
+ `markdown-trees-clean/` holds two files, each allowed for a DIFFERENT reason —
7
+ an allowed tree and a control artifact (`backend/AGENTS.md`) — so that a gate
8
+ reporting every `.md` it sees would fail it. (The crate test fixture it used to
9
+ hold was a consumer-layout exemption, deleted by PBI 598.)
10
10
 
11
11
  This check is not yet wired into `checks/__init__.py` (a later step wires this
12
12
  port's whole wave into the registry in one shot, to avoid concurrent edits to
@@ -40,7 +40,6 @@ CLEAN = FIXTURES / "markdown-trees-clean"
40
40
 
41
41
  STRAY_FILES = ("backend/notes.md", "docs/page.md")
42
42
  CLEAN_FILES = (
43
- "backend/crates/enact-example/tests/fixtures/sample.md",
44
43
  "backend/AGENTS.md",
45
44
  "docs/page.md",
46
45
  )
@@ -54,9 +53,6 @@ def graded(root: pathlib.Path, given: tuple[str, ...]):
54
53
  inputs={
55
54
  "docs_root": root / "docs",
56
55
  "plans_root": root / "plans",
57
- "specs_root": root / ".specs",
58
- "rust_root": root / "backend",
59
- "frontend_root": root / "frontend",
60
56
  "workflows_root": root / ".workflows",
61
57
  },
62
58
  )
@@ -91,13 +87,26 @@ def test_a_clean_corpus_of_allowed_and_excluded_paths_reports_nothing() -> None:
91
87
 
92
88
 
93
89
  def test_each_exclusion_family_is_independently_load_bearing() -> None:
94
- """Without this, a corpus of nothing but allowed-tree files also passes
95
- against a gate that reports every .md it is handed, and either exclusion
96
- family (control artifact, crate test fixture) could rot unnoticed."""
90
+ """A corpus of only allowed-tree files must not pass a gate that reports every .md."""
97
91
  assert graded(CLEAN, ("backend/AGENTS.md",)).findings == ()
98
- assert graded(
99
- CLEAN, ("backend/crates/enact-example/tests/fixtures/sample.md",)
100
- ).findings == ()
92
+
93
+
94
+ def test_a_workflows_coverage_note_is_no_longer_excluded() -> None:
95
+ """Deliberate: the exemption named one consumer's file; the tree is graded like any other."""
96
+ verdict = graded(CLEAN, (".workflows/COVERAGE.md",))
97
+ assert [f.path for f in verdict.findings] == [".workflows/COVERAGE.md"]
98
+
99
+
100
+ def test_former_rust_and_frontend_exclusions_are_gone() -> None:
101
+ """PBI 598: the crate-fixture, frontend and session-prompt exclusions are deleted with their trees."""
102
+ former = (
103
+ "backend/crates/x/tests/fixtures/sample.md",
104
+ "backend/crates/x/tests/a/fixture-b/sample.md",
105
+ "frontend/.claude/notes.md",
106
+ "frontend/examples/demo.md",
107
+ )
108
+ verdict = graded(CLEAN, former)
109
+ assert sorted(f.path for f in verdict.findings) == sorted(former), verdict.render()
101
110
 
102
111
 
103
112
  # ------------------------------------------- intersection, half one: the set
@@ -131,7 +140,7 @@ def test_a_register_in_the_declared_compliance_tree_is_allowed(tmp_path) -> None
131
140
  """A markdown register belongs beside the data it registers.
132
141
 
133
142
  `ALLOWED` is a fixed tuple, so before this the only homes for a `.md` file
134
- were `docs/`, `plans/`, `specs/` and `.changeset/` — and a repository
143
+ were `docs/` and `plans/` — and a repository
135
144
  declaring `compliance = "compliance"` (the tree holding its own ratchets)
136
145
  could not put a register there without tripping this gate. The tree is the
137
146
  REPOSITORY's declaration; hardcoding `compliance/*.md` here would have been
@@ -186,8 +195,8 @@ def test_a_redeclared_estate_still_allows_and_excludes_the_resolved_entries(
186
195
  ) -> None:
187
196
  """The LOUD false-finding this check shipped with.
188
197
 
189
- `docs/*.md`/`docs/*.mdx`, `plans/*.md`, `.specs/*.md` in ALLOWED, and the
190
- `backend/...`/`frontend/...`/`.workflows/...` entries in EXCLUDED, were
198
+ `docs/*.md`/`docs/*.mdx` and `plans/*.md` in ALLOWED, and the
199
+ `.workflows/...` entries in EXCLUDED, were
191
200
  all hardcoded literals. A repository whose corresponding root is not the
192
201
  packaged default had a real, legitimately-admitted or -excluded file fail
193
202
  every one of those patterns and get reported "outside the allowed docs
@@ -199,21 +208,13 @@ def test_a_redeclared_estate_still_allows_and_excludes_the_resolved_entries(
199
208
  given = (
200
209
  "documentation/page.md",
201
210
  "roadmap/plan.md",
202
- "specifications/note.md",
203
- "webapp/.claude/notes.md",
204
211
  "automation/changesets/note.md",
205
- "rustcode/crates/enact-example/tests/fixture-a/sample.md",
206
- "webapp/examples/demo.md",
207
- "automation/COVERAGE.md",
208
212
  )
209
213
  ctx = Context(
210
214
  root=tmp_path, row=ROW,
211
215
  inputs={
212
216
  "docs_root": tmp_path / "documentation",
213
217
  "plans_root": tmp_path / "roadmap",
214
- "specs_root": tmp_path / "specifications",
215
- "rust_root": tmp_path / "rustcode",
216
- "frontend_root": tmp_path / "webapp",
217
218
  "workflows_root": tmp_path / "automation",
218
219
  },
219
220
  )
@@ -269,13 +270,11 @@ def test_a_declared_root_name_is_excluded_at_the_root_only(tmp_path) -> None:
269
270
  assert [f.path for f in verdict.findings] == ["backend/STATUS.md"], verdict.render()
270
271
 
271
272
 
272
- def test_a_declared_name_is_matched_literally_not_as_a_glob(tmp_path) -> None:
273
- """`_matches_any` uses `fnmatchcase`, so a declared name carrying glob
274
- characters must be escaped: `a[b].md` excludes that file and not `ab.md`."""
275
- verdict = graded_declaring(tmp_path, {"root-files": "a[b].md"}, ("a[b].md", "ab.md"))
276
- assert [f.path for f in verdict.findings] == ["ab.md"], verdict.render()
277
- verdict = graded_declaring(tmp_path, {"root-files": "*.md"}, ("anything.md",))
278
- assert [f.path for f in verdict.findings] == ["anything.md"], verdict.render()
273
+ def test_a_declared_name_carrying_glob_characters_is_could_not_run(tmp_path) -> None:
274
+ """A glob in `root-files` is refused for every gate that reads it, not matched literally."""
275
+ for bad in ("a[b].md", "*.md"):
276
+ with pytest.raises(CouldNotRun, match="root-files"):
277
+ graded_declaring(tmp_path, {"root-files": bad}, ("a[b].md", "ab.md"))
279
278
 
280
279
 
281
280
  def test_a_root_dirs_declaration_does_not_exclude_a_root_markdown_file(tmp_path) -> None:
@@ -412,3 +411,7 @@ def test_a_malformed_declared_markdown_root_is_could_not_run_through_the_runner(
412
411
  )
413
412
  assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
414
413
  assert DECLARED_ROOTS in verdict.message
414
+
415
+
416
+ def test_widening_inputs_name_only_declared_inputs() -> None:
417
+ assert set(MarkdownTrees.widening_inputs) <= set(MarkdownTrees.inputs)
@@ -1,43 +1,26 @@
1
- """LAY-008 — reject out-of-place STAGED files, by four independent rules.
1
+ """LAY-008 — reject stray artifacts: throwaway files and undeclared images.
2
2
 
3
- Ported from `compliance_new/checks/layout/placement/check.sh`, which grades
4
- the given set only (K-29..K-31): no walk, no git call, no self-location. The
5
- row is entirely `graded()` — every rule is answered by the path SHAPE of a
6
- member of the given set, never by probing a named path, so `structural()` has
7
- nothing to contribute.
3
+ Ported from `compliance_new/checks/layout/placement/check.sh`, which graded the
4
+ given set only: no walk, no git call, no self-location. The row is entirely
5
+ `graded()` — the rule is answered by the path SHAPE of a member of the given
6
+ set, so `structural()` has nothing to contribute.
8
7
 
9
- Four rules, evaluated independently per path (a single path can trip more than
10
- one):
8
+ One rule, evaluated per path (Tarun, 2026-10-08, PBI 599/601). The shell script's
9
+ other rules are deleted: the root allowlist duplicated LAY-012 (`root-closed-set`),
10
+ and "`apps/` is retired" was one monorepo's history.
11
11
 
12
- 1. root allowlist a file with no `/` must be on the allowlist of files
13
- legitimately committed at the repository root: the
14
- central set in `policy/root_shape.py` plus whatever the
15
- repository itself declares (`root-files`).
16
- 2. apps/ retired any path under the retired `apps/` tree -- unless the
17
- repository declares `apps` among its root directories
18
- (`root-dirs`), which says `apps/` is live there.
19
- 3. stray artifacts a throwaway extension (`.log`/`.tmp`/`.bak`/`.swp`,
20
- checked STRICTLY, everywhere) or a screenshot/image
21
- basename outside the enumerated asset directories.
22
- 4. md sprawl a `.md` file inside `backend/` or `agent/`'s `src/`
23
- tree, except the two exempted files.
12
+ stray artifacts a throwaway extension (`.log`/`.tmp`/`.bak`/`.swp`, checked
13
+ STRICTLY, everywhere, asset directories included) or a
14
+ screenshot/image basename outside the asset directories the
15
+ REPOSITORY DECLARES.
24
16
 
25
- **One rule id for all four, and it does not match this row.** The shell
26
- `check.sh` hardcodes `RULE_ID=LAY-007` — but `LAY-007` is a DIFFERENT row
27
- (`layout.pipeline-templates`); this row's own id is `LAY-008`. That is exactly
28
- the "a check never spells its own id" defect `compliance_new/AGENTS.md`
29
- documents for `FRM-009`/`FRM-010` (both hardcoded a rule literal that had
30
- drifted from their row). It is reproduced here rather than silently
31
- corrected, because the port's job is the shell tree's ACTUAL behaviour —
32
- `test.sh` asserts findings prefixed `^LAY-007 ` — not the behaviour it should
33
- have had; the mismatch is called out in the porting notes instead.
17
+ The declaration is `asset-dirs` (`--declare asset-dirs=a/b,c`, from
18
+ `[controls.compliance.placement] asset_dirs`), a comma-separated list of
19
+ repository-relative directories. An image anywhere under one is admitted; the
20
+ registry names no consumer layout, so with no declaration every image is reported.
34
21
 
35
- The shell source's own comments and finding messages have a second, unrelated
36
- defect: substring-corrupted prose (`"legitimate"` reads `"leimate"`, the
37
- throwaway/screenshot messages read `"-artifact:"` with their lead word gone
38
- entirely). Message TEXT is not the rule the row grades, so this port writes
39
- clean prose rather than reproducing the corruption; the logic — which
40
- extensions, which prefixes, which exemptions — is carried over faithfully.
22
+ **The rule id.** The shell `check.sh` hardcoded `RULE_ID=LAY-007`, a DIFFERENT
23
+ row's id; this port reports under this row's own `LAY-008`.
41
24
  """
42
25
 
43
26
  from __future__ import annotations
@@ -48,174 +31,48 @@ from typing import ClassVar
48
31
 
49
32
  from ....core.check import Check
50
33
  from ....core.context import Context, FileSet
34
+ from ....core.errors import CouldNotRun
51
35
  from ....core.finding import Report
52
- from ....policy.root_shape import (
53
- ALLOWED_ROOT_FILES,
54
- ROOT_DIRS_DECLARATION,
55
- allowed_root_files,
56
- declared_root_names,
57
- )
36
+ from ....policy.root_shape import declared_root_names
58
37
 
59
38
 
60
39
  class Placement(Check):
61
40
  id: ClassVar[str] = "LAY-008"
62
- # Matches check.sh's hardcoded RULE_ID exactly (see module docstring: this
63
- # is the shell tree's real, if mismatched, behaviour — not a typo here).
64
- # Was "LAY-007" — a copy-paste that printed every finding of this gate under
65
- # the neighbouring gate's id, so a reader chasing a placement violation was
66
- # sent to the pipeline-templates gate instead.
67
41
  rule: ClassVar[str] = "LAY-008"
68
42
 
69
- # The shared root policy, not a fourth copy of it. This gate's own list had
70
- # already drifted from LAY-012's: it still carried the shell script's typos
71
- # `.attributes` and `.ignore`, which LAY-012 had long since corrected to
72
- # `.gitattributes` and `.gitignore` — so the same file was permitted by one
73
- # gate and reported by the other.
74
- #
75
- # This is the CENTRAL default only. What is actually graded is the effective
76
- # set, `allowed_root_files(ctx.declarations)`: this plus the names the
77
- # repository declares under `[controls.compliance.root] files`.
78
- ROOT_ALLOWLIST: ClassVar[frozenset[str]] = ALLOWED_ROOT_FILES
43
+ # `[controls.compliance.placement] asset_dirs`, sent as `--declare asset-dirs=a,b`.
44
+ ASSET_DIRS_DECLARATION: ClassVar[str] = "asset-dirs"
79
45
 
80
46
  THROWAWAY_SUFFIXES: ClassVar[tuple[str, ...]] = (
81
47
  ".log", ".LOG", ".tmp", ".TMP", ".bak", ".BAK", ".swp", ".SWP",
82
48
  )
83
49
 
84
- inputs: ClassVar[dict[str, str]] = {
85
- "docs_root": "<docs>", "rust_root": "<rust>",
86
- "frontend_root": "<frontend>",
87
- }
88
- """FIXED 2026-09-16 (K-42 sweep, approved): every `docs/...`/`backend/...`/
89
- `.specs/...`/`frontend/...` entry below (ASSET_DIR_PREFIXES,
90
- ASSET_EXACT_FILES, MD_SRC_EXEMPT, MD_SRC_TREES's `backend/`) was a
91
- hardcoded literal. `_asset_exempt` is a LOUD shape -- a repository whose
92
- root is not the packaged default had a real, legitimately-placed asset
93
- fail the exemption and get reported "looks like a screenshot", a visible
94
- wrong finding. `MD_SRC_TREES`/`MD_SRC_EXEMPT` are the opposite, SILENT
95
- shape: a redeclared rust root meant `_md_sprawl`'s own `startswith` never
96
- matched, so real `.md` sprawl under that tree went unreported entirely.
97
- `agent/` (MD_SRC_TREES, MD_SRC_EXEMPT) stays literal -- there is no
98
- `<agent>` tree in `policy/config.toml`. Every other suffix stays literal
99
- below its resolved root: named subject matter, not a resolvable path."""
100
-
101
- # Enumerated product asset directories — a screenshot/image basename is
102
- # legitimate ONLY under one of these (a directory prefix) or as this one
103
- # exact file. Checked ahead of the screenshot/image basename test, exactly
104
- # as check.sh's nested `case "$f"` does before falling through to
105
- # `case "$base"`.
106
- ASSET_DIR_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
107
- ("docs_root", "assets/research/mobile-reference/"),
108
- ("rust_root", "crates/enact-app/assets/builtin-assistants/avatars/"),
109
- ("rust_root", "crates/enact-assets/assets/logos/"),
110
- ("frontend_root", "mobile/assets/images/"),
111
- ("frontend_root", "packages/desktop/src/renderer/assets/logos/"),
112
- ("frontend_root", "packages/desktop/src/renderer/assets/themes/"),
113
- ("frontend_root", "public/pwa/"),
114
- ("frontend_root", "resources/"),
115
- )
116
- ASSET_EXACT_FILE_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
117
- ("frontend_root", "tests/e2e/helpers/screenshots.ts"),
118
- )
119
-
120
50
  SCREENSHOT_PATTERN: ClassVar[re.Pattern[str]] = re.compile("screenshot", re.IGNORECASE)
121
51
  IMAGE_SUFFIXES: ClassVar[tuple[str, ...]] = (
122
52
  ".png", ".PNG", ".jpg", ".JPG", ".jpeg", ".JPEG",
123
53
  )
124
54
 
125
- MD_SRC_EXEMPT_LITERAL: ClassVar[tuple[str, ...]] = (
126
- "agent/crates/enact-providers/src/snapshots/README.md",
127
- )
128
- MD_SRC_EXEMPT_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
129
- ("rust_root", "crates/enact-session/src/backend/codex_init_prompt.md"),
130
- )
131
- MD_SRC_TREES_LITERAL: ClassVar[tuple[str, ...]] = ("agent/",)
132
-
133
- def _asset_dir_prefixes(self, ctx: Context) -> tuple[str, ...]:
134
- return tuple(
135
- f"{ctx.relative(ctx.inputs[input_name])}/{suffix}"
136
- for input_name, suffix in self.ASSET_DIR_TEMPLATES
137
- if input_name in ctx.inputs # a widening input: absent -> no entry
138
- )
139
-
140
- def _asset_exact_files(self, ctx: Context) -> tuple[str, ...]:
141
- return tuple(
142
- f"{ctx.relative(ctx.inputs[input_name])}/{suffix}"
143
- for input_name, suffix in self.ASSET_EXACT_FILE_TEMPLATES
144
- if input_name in ctx.inputs
145
- )
146
-
147
- def _md_src_exempt(self, ctx: Context) -> tuple[str, ...]:
148
- return self.MD_SRC_EXEMPT_LITERAL + tuple(
149
- f"{ctx.relative(ctx.inputs[input_name])}/{suffix}"
150
- for input_name, suffix in self.MD_SRC_EXEMPT_TEMPLATES
151
- if input_name in ctx.inputs
152
- )
153
-
154
- def _md_src_trees(self, ctx: Context) -> tuple[str, ...]:
155
- # Decision for a tree declared absent (`rust = ""`, which drops
156
- # `rust_root` from the resolved inputs): the rust root only contributes
157
- # an extra PREFIX to the trees whose `src/` Markdown is reported, so a
158
- # repository with no Rust tree has nothing under it to report and
159
- # contributes nothing. The literal `agent/` tree still governs. This read
160
- # `ctx.inputs['rust_root']` unguarded -- a `KeyError` traceback (exit 1),
161
- # not a verdict, on any `.md` path.
162
- rust_root = ctx.inputs.get("rust_root")
163
- if rust_root is None:
164
- return self.MD_SRC_TREES_LITERAL
165
- return self.MD_SRC_TREES_LITERAL + (f"{ctx.relative(rust_root)}/",)
166
-
167
55
  def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
168
- # `LC_ALL=C sort` in the shell is a byte-order sort; Python's default
169
- # string ordering matches it for the ASCII repository-relative paths
170
- # this row ever sees.
56
+ asset_dirs = self._asset_dirs(ctx)
171
57
  for relative in sorted(files):
172
- yield from self._root_allowlist(ctx, relative)
173
- yield from self._apps_retired(ctx, relative)
174
- yield from self._stray_artifact(ctx, relative)
175
- yield from self._md_sprawl(ctx, relative)
176
-
177
- # -- Rule 1: root-level allowlist ---------------------------------------
178
-
179
- def _root_allowlist(self, ctx: Context, relative: str) -> Iterable[Report]:
180
- if "/" in relative:
181
- return
182
- if relative in allowed_root_files(ctx.declarations):
183
- return
184
- # The allowlist is ~60 entries. Printing all of them per finding buries
185
- # the one fact the line is reporting, and five findings produced three
186
- # hundred lines of identical noise.
187
- yield self.report(
188
- relative,
189
- f"root: '{relative}' is not on the root allowlist",
190
- f"move '{relative}' into an owning subdirectory, or declare it under "
191
- "[controls.compliance.root] files in this repository's adl-config.toml "
192
- "if it genuinely belongs at the root",
193
- )
194
-
195
- # -- Rule 2 (numbered 4 in check.sh): apps/ is retired ------------------
196
-
197
- def _apps_retired(self, ctx: Context, relative: str) -> Iterable[Report]:
198
- # A repository that declares `apps` as a root directory has said `apps/`
199
- # is live there; "apps is retired" would contradict its own declaration.
200
- if "apps" in declared_root_names(ctx.declarations, ROOT_DIRS_DECLARATION):
201
- return
202
- if relative.startswith("apps/"):
203
- yield self.report(
204
- relative,
205
- f"apps-retired: '{relative}' is staged under retired apps/; "
206
- "use the owning current tree",
207
- f"move '{relative}' into frontend/ (or the current owning tree) "
208
- "and remove it from apps/",
209
- )
210
-
211
- # -- Rule 3 (numbered 2 in check.sh): stray test/ artifacts -------------
212
-
213
- def _stray_artifact(self, ctx: Context, relative: str) -> Iterable[Report]:
214
- # check.sh's `case "$f" in /*) ;; *) ... esac` skips this whole rule
215
- # for a path beginning with a literal `/`. The given set is always
216
- # repository-relative (never absolute), so this arm is inert in
217
- # practice — carried over rather than dropped, since it is what the
218
- # shell source actually branches on.
58
+ yield from self._stray_artifact(relative, asset_dirs)
59
+
60
+ def _asset_dirs(self, ctx: Context) -> tuple[str, ...]:
61
+ """The directories the repository declared as homes for images; malformed ones are could-not-run."""
62
+ dirs: list[str] = []
63
+ for name in sorted(declared_root_names(ctx.declarations, self.ASSET_DIRS_DECLARATION)):
64
+ directory = name.rstrip("/")
65
+ if name.startswith("/") or any(part in ("", ".", "..") for part in directory.split("/")):
66
+ raise CouldNotRun(
67
+ f"{self.id}: `{self.ASSET_DIRS_DECLARATION}` declares {name!r}, which is not "
68
+ "a repository-relative directory; fix [controls.compliance.placement] "
69
+ "asset_dirs in adl-config.toml"
70
+ )
71
+ dirs.append(directory)
72
+ return tuple(dirs)
73
+
74
+ def _stray_artifact(self, relative: str, asset_dirs: tuple[str, ...]) -> Iterable[Report]:
75
+ # check.sh skipped a leading `/`; inert here (paths are repo-relative), kept from the shell source.
219
76
  if relative.startswith("/"):
220
77
  return
221
78
 
@@ -231,38 +88,14 @@ class Placement(Check):
231
88
  )
232
89
  return
233
90
 
234
- if self._asset_exempt(ctx, relative):
91
+ if any(relative.startswith(f"{directory}/") for directory in asset_dirs):
235
92
  return
236
93
 
237
94
  if self.SCREENSHOT_PATTERN.search(base) or base.endswith(self.IMAGE_SUFFIXES):
238
95
  yield self.report(
239
96
  relative,
240
- f"'{relative}' looks like a screenshot — belongs in "
241
- ".gitignore, not committed",
242
- f"move '{relative}' under a gitignored path or into one of "
243
- "the enumerated asset directories, then unstage it from the commit",
97
+ f"'{relative}' looks like a screenshot or image outside the declared "
98
+ "asset directories — belongs in .gitignore or an asset directory",
99
+ f"move '{relative}' under a gitignored path, or declare its directory under "
100
+ "[controls.compliance.placement] asset_dirs in this repository's adl-config.toml",
244
101
  )
245
-
246
- def _asset_exempt(self, ctx: Context, relative: str) -> bool:
247
- if relative in self._asset_exact_files(ctx):
248
- return True
249
- return any(relative.startswith(prefix) for prefix in self._asset_dir_prefixes(ctx))
250
-
251
- # -- Rule 4 (numbered 3 in check.sh): .md sprawl into backend//agent/ src/ --
252
-
253
- def _md_sprawl(self, ctx: Context, relative: str) -> Iterable[Report]:
254
- if not relative.endswith(".md"):
255
- return
256
- if relative in self._md_src_exempt(ctx):
257
- return
258
- if not relative.startswith(self._md_src_trees(ctx)):
259
- return
260
- if "/src/" not in relative:
261
- return
262
- yield self.report(
263
- relative,
264
- f"md-sprawl: '{relative}' is a markdown file inside a src/ tree "
265
- "— narrative docs belong in docs/ or plans/",
266
- f"move '{relative}' into docs/ or plans/, or add it to check.py's "
267
- "src/ Markdown exemption list if it must stay",
268
- )