@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.30 → 0.1.33

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 (199) 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/adl-git-push-policy.ts +5 -4
  14. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  15. package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
  16. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +19 -5
  17. package/packages/agent-settings/assets/GLOBAL-AGENTS.md +1 -1
  18. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
  19. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
  20. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
  21. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
  22. package/packages/azdo/src/lib/commit-types.ts +8 -0
  23. package/packages/azdo/src/lib/validate-pr-metadata.ts +4 -1
  24. package/packages/fleet/manifest.json +2 -1
  25. package/packages/fleet/src/repo-maintain.ts +5 -2
  26. package/packages/repo-controls/AGENTS.md +8 -5
  27. package/packages/repo-controls/README.md +348 -58
  28. package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
  29. package/packages/repo-controls/assets/promote.ts +7 -85
  30. package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
  31. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
  32. package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
  33. package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
  34. package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
  35. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
  36. package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
  37. package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
  38. package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
  39. package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
  40. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
  41. package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
  42. package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
  43. package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
  44. package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
  45. package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
  46. package/packages/repo-controls/assets/version-bump.sh +7 -48
  47. package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
  48. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
  49. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
  50. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
  51. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
  52. 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
  53. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
  54. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
  55. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
  56. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
  57. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
  58. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
  59. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
  60. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
  61. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
  62. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
  63. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
  72. 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
  73. 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
  74. 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
  75. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
  76. 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
  77. 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
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
  92. 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
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
  94. 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
  95. 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
  96. 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
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
  98. 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
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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
  106. 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
  107. 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
  108. 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
  109. 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
  110. 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
  111. 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
  112. 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
  113. 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
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
  124. 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
  125. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
  126. 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
  127. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
  128. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
  129. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
  130. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
  131. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
  132. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
  133. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
  134. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
  135. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
  136. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
  137. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
  138. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
  139. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
  140. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
  141. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
  142. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
  143. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
  144. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
  145. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
  146. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
  147. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
  148. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
  149. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
  150. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
  151. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
  152. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
  153. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
  154. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
  155. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
  156. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
  157. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
  158. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
  159. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
  160. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
  161. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
  162. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
  163. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
  164. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
  179. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
  180. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
  181. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  182. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
  183. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
  184. package/packages/repo-controls/scripts/hooks/commit-msg +1 -1
  185. package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
  186. package/packages/repo-controls/scripts/hooks/pre-push +27 -19
  187. package/packages/repo-controls/src/cli.ts +22 -3
  188. package/packages/repo-controls/src/compliance-config.ts +132 -65
  189. package/packages/repo-controls/src/config.ts +62 -52
  190. package/packages/repo-controls/src/constants.ts +9 -0
  191. package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
  192. package/packages/repo-controls/src/doctor.ts +20 -30
  193. package/packages/repo-controls/src/findings-ratchet.ts +14 -0
  194. package/packages/repo-controls/src/gitignore-block.ts +3 -13
  195. package/packages/repo-controls/src/hook-log.ts +7 -10
  196. package/packages/repo-controls/src/hook.ts +127 -83
  197. package/packages/repo-controls/src/install-compliance.ts +112 -0
  198. package/packages/repo-controls/src/install.ts +133 -30
  199. package/packages/repo-controls/src/integrity.ts +5 -18
@@ -1,10 +1,11 @@
1
- """LAY-008 — ported from `compliance_new/checks/layout/placement/test.sh`.
1
+ """LAY-008 — ported from `compliance_new/checks/layout/placement/test.sh`; rebuilt to its
2
+ throwaway-and-image rule only (Tarun 2026-10-08: PBI 599/601).
2
3
 
3
- Two corpora, same names and file shapes as the shell fixtures:
4
- `placement-allowed` (the 11 enumerated product paths that must stay silent)
5
- and `placement-stray` (one path per rule that must fire: a screenshot outside
6
- every enumerated asset directory, a markdown file inside a crate `src/` tree,
7
- and a file under retired `apps/`).
4
+ Two corpora, same names as the shell fixtures: `placement-allowed` (paths that
5
+ must stay silent) and `placement-stray` (one path per rule that must fire: a
6
+ screenshot and a file under retired `apps/`). PBI 598 (Tarun, 2026-10-08)
7
+ deleted the enumerated product asset directories and the `src/` Markdown sprawl
8
+ rule, so the allowed corpus no longer holds product-asset paths.
8
9
 
9
10
  The shell `test.sh` drives `check.sh` DIRECTLY with the whole corpus as
10
11
  `ADL_CHANGED_FILES` — it does not go through `gate-runner.sh`'s row-scope
@@ -61,17 +62,7 @@ def _whole_corpus(root: pathlib.Path) -> tuple[str, ...]:
61
62
  def graded_directly(root: pathlib.Path, paths: tuple[str, ...]) -> Verdict:
62
63
  """Bypasses row-scope narrowing entirely — the check's OWN filtering
63
64
  only. Mirrors the shell test's direct `check.sh` invocation."""
64
- # Every root is a RESOLVED INPUT now, not a class constant -- the packaged
65
- # defaults, which every fixture below already uses.
66
- ctx = Context(
67
- root=root, row=ROW,
68
- inputs={
69
- "docs_root": root / "docs",
70
- "rust_root": root / "backend",
71
- "frontend_root": root / "frontend",
72
- "specs_root": root / ".specs",
73
- },
74
- )
65
+ ctx = Context(root=root, row=ROW)
75
66
  findings = tuple(Placement().graded(ctx, FileSet(paths=paths)))
76
67
  return decide(ROW, findings, 0, ())
77
68
 
@@ -89,82 +80,72 @@ def run_via_row_scope(
89
80
  f"engine.scope matches no file on disk ({' | '.join(ROW.engine.scope)}); "
90
81
  "a scope selecting nothing is could-not-run, never a clean pass",
91
82
  )
92
- # Every root is a RESOLVED INPUT now, not a class constant -- the packaged
93
- # defaults, which every fixture below already uses.
94
- ctx = Context(
95
- root=root, row=ROW,
96
- inputs={
97
- "docs_root": root / "docs",
98
- "rust_root": root / "backend",
99
- "frontend_root": root / "frontend",
100
- "specs_root": root / ".specs",
101
- },
102
- )
83
+ ctx = Context(root=root, row=ROW)
103
84
  findings = tuple(Placement().run(ctx, narrowed))
104
85
  return decide(ROW, findings, 0, ())
105
86
 
106
87
 
107
- def test_the_enumerated_product_paths_are_all_silent() -> None:
108
- """Every allowed path is actually graded, not narrowed out of the set —
109
- all 11 enumerated paths, exactly as the shell test asserts, or a
110
- corpus drift would go unnoticed."""
88
+ def test_the_allowed_corpus_is_all_silent() -> None:
89
+ """Every allowed path is actually graded, not narrowed out of the set."""
111
90
  given = _whole_corpus(ALLOWED)
112
- assert len(given) == 10, given
91
+ assert len(given) == 1, given
113
92
  verdict = graded_directly(ALLOWED, given)
114
93
  assert verdict.outcome is Outcome.CLEAN, verdict.render()
115
94
  assert verdict.findings == ()
116
95
 
117
96
 
97
+ def test_an_image_in_a_former_product_asset_directory_is_now_reported() -> None:
98
+ """PBI 598: no hardcoded asset directory; a formerly exempt path is a stray image."""
99
+ former = (
100
+ "frontend/public/pwa/icon-192.png",
101
+ "backend/crates/assets/logos/tools/coding/tool.png",
102
+ "docs/assets/research/mobile-reference/shot.png",
103
+ )
104
+ verdict = graded_directly(ALLOWED, former)
105
+ assert sorted(f.path for f in verdict.findings) == sorted(former), verdict.render()
106
+
107
+
118
108
  def test_out_of_place_paths_are_reported() -> None:
109
+ """PBI 599/601: apps/ is no longer retired, so only the screenshot is reported."""
119
110
  given = _whole_corpus(STRAY)
120
- assert len(given) == 3, given
111
+ assert len(given) == 2, given
121
112
  verdict = graded_directly(STRAY, given)
122
113
  assert verdict.outcome is Outcome.FINDINGS
123
- assert len(verdict.findings) == 3, verdict.findings
114
+ assert [f.path for f in verdict.findings] == ["frontend/unapproved/screenshot.png"]
124
115
  # LAY-008, not LAY-007: the check used to report under its neighbour's id,
125
116
  # and this assertion pinned the bug in place.
126
117
  assert all(f.rule == "LAY-008" for f in verdict.findings)
127
118
 
128
119
 
129
- def test_a_screenshot_outside_every_enumerated_asset_directory_is_reported() -> None:
120
+ def test_an_undeclared_screenshot_is_reported() -> None:
130
121
  given = _whole_corpus(STRAY)
131
122
  verdict = graded_directly(STRAY, given)
132
123
  paths = {f.path for f in verdict.findings}
133
124
  assert "frontend/unapproved/screenshot.png" in paths
134
125
 
135
126
 
136
- def test_a_markdown_file_inside_a_crate_src_tree_is_reported() -> None:
137
- given = _whole_corpus(STRAY)
138
- verdict = graded_directly(STRAY, given)
139
- paths = {f.path for f in verdict.findings}
140
- assert "backend/crates/enact-session/src/backend/README.md" in paths
141
-
142
-
143
- def test_a_file_under_retired_apps_is_reported() -> None:
144
- given = _whole_corpus(STRAY)
145
- verdict = graded_directly(STRAY, given)
146
- paths = {f.path for f in verdict.findings}
147
- assert "apps/legacy/file.ts" in paths
148
-
149
-
150
127
  def test_a_throwaway_extension_is_reported_strictly_everywhere() -> None:
151
- """Rule 3's throwaway-extension arm fires even inside an enumerated asset
152
- directory — check.sh's own comment: checked "strictly everywhere",
153
- ahead of the asset-directory exemption."""
128
+ """Rule 3 (throwaway extensions) fires everywhere, per check.sh."""
154
129
  verdict = graded_directly(
155
- ALLOWED, ("frontend/resources/build.log",),
130
+ ALLOWED, ("assets/build.log",),
156
131
  )
157
132
  assert verdict.outcome is Outcome.FINDINGS
158
133
  assert len(verdict.findings) == 1
159
- assert verdict.findings[0].path == "frontend/resources/build.log"
134
+ assert verdict.findings[0].path == "assets/build.log"
160
135
  assert "throwaway artifact" in verdict.findings[0].message
161
136
 
162
137
 
163
- def test_a_root_level_file_off_the_allowlist_is_reported() -> None:
164
- verdict = graded_directly(ALLOWED, ("random.txt",))
165
- assert verdict.outcome is Outcome.FINDINGS
166
- assert verdict.findings[0].path == "random.txt"
167
- assert "root allowlist" in verdict.findings[0].message
138
+ def test_rule_1_the_root_allowlist_is_gone() -> None:
139
+ """PBI 599/601: the root allowlist moved to LAY-012, so a root file off every list is silent here."""
140
+ verdict = graded_directly(ALLOWED, ("random.txt", "STATUS.md"))
141
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
142
+ assert not hasattr(Placement, "ROOT_ALLOWLIST")
143
+
144
+
145
+ def test_rule_2_apps_is_not_retired() -> None:
146
+ """PBI 599/601: a file under `apps/` is graded like any other path."""
147
+ verdict = graded_directly(ALLOWED, ("apps/legacy/file.ts", "apps/cli/src/main.ts"))
148
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
168
149
 
169
150
 
170
151
  def test_a_root_level_allowlisted_file_is_silent() -> None:
@@ -180,159 +161,60 @@ def test_no_set_is_could_not_run() -> None:
180
161
  assert verdict.outcome is Outcome.COULD_NOT_RUN
181
162
 
182
163
 
183
- def test_a_redeclared_estate_still_exempts_a_real_asset(tmp_path: pathlib.Path) -> None:
184
- """The LOUD false-finding ASSET_DIR_TEMPLATES shipped with.
185
-
186
- Every entry was a hardcoded literal, tested with `startswith`/`==` in
187
- `_asset_exempt`. A repository whose frontend root is not the packaged
188
- default had a real, legitimately-placed themed asset fail that
189
- exemption and get reported "looks like a screenshot" -- a visible,
190
- wrong finding about a file the exemption exists to admit."""
191
- ctx = Context(
192
- root=tmp_path, row=ROW,
193
- inputs={
194
- "docs_root": tmp_path / "docs",
195
- "rust_root": tmp_path / "backend",
196
- "frontend_root": tmp_path / "webapp",
197
- "specs_root": tmp_path / ".specs",
198
- },
199
- )
200
- path = "webapp/packages/desktop/src/renderer/assets/themes/screenshot-dark.png"
201
- findings = tuple(Placement().graded(ctx, FileSet(paths=(path,))))
202
- assert findings == (), (
203
- f"a real themed asset under a redeclared frontend root was wrongly "
204
- f"reported as a stray screenshot: {findings}"
205
- )
206
-
207
-
208
- def test_a_redeclared_rust_root_still_catches_md_sprawl(tmp_path: pathlib.Path) -> None:
209
- """The SILENT false pass MD_SRC_TREES/MD_SRC_EXEMPT shipped with.
210
-
211
- `MD_SRC_TREES`'s `backend/` entry was a hardcoded literal, tested with
212
- `startswith` in `_md_sprawl`. A repository whose rust root is not the
213
- packaged default had real narrative-doc sprawl inside its `src/` tree
214
- fail that `startswith` test, and the gate never examined it at all: no
215
- error, no finding, indistinguishable from a repository with no sprawl."""
216
- ctx = Context(
217
- root=tmp_path, row=ROW,
218
- inputs={
219
- "docs_root": tmp_path / "docs",
220
- "rust_root": tmp_path / "rustcode",
221
- "frontend_root": tmp_path / "frontend",
222
- "specs_root": tmp_path / ".specs",
223
- },
224
- )
225
- path = "rustcode/crates/enact-example/src/notes.md"
226
- findings = tuple(Placement().graded(ctx, FileSet(paths=(path,))))
227
- assert any("md-sprawl" in f.message for f in findings), (
228
- f"real markdown sprawl under a redeclared rust root went "
229
- f"unreported: {findings}"
230
- )
231
-
232
-
233
- # ---------------------------------------------- per-repository root declarations
164
+ # ----------------------------------------------- the repository's declared asset dirs
234
165
  def graded_declaring(
235
166
  declarations: dict[str, str], paths: tuple[str, ...], root: pathlib.Path = ALLOWED
236
167
  ) -> Verdict:
237
- """`graded_directly`, plus what the repository declared about its own root
238
- (`[controls.compliance.root]` arrives as the `root-files` / `root-dirs`
239
- declarations)."""
240
- ctx = Context(
241
- root=root, row=ROW, declarations=declarations,
242
- inputs={
243
- "docs_root": root / "docs",
244
- "rust_root": root / "backend",
245
- "frontend_root": root / "frontend",
246
- "specs_root": root / ".specs",
247
- },
248
- )
168
+ """`graded_directly` plus the repository declarations (`asset_dirs` arrives as `asset-dirs`)."""
169
+ ctx = Context(root=root, row=ROW, declarations=declarations)
249
170
  findings = tuple(Placement().graded(ctx, FileSet(paths=paths)))
250
171
  return decide(ROW, findings, 0, ())
251
172
 
252
173
 
253
- def test_a_non_standard_root_file_is_reported_without_a_declaration() -> None:
254
- """STATUS.md is not on the central list any more: without the repository
255
- declaring it, it is an undeclared root file."""
256
- verdict = graded_declaring({}, ("STATUS.md",))
257
- assert verdict.outcome is Outcome.FINDINGS, verdict.render()
258
- assert [f.path for f in verdict.findings] == ["STATUS.md"]
259
- assert "root allowlist" in verdict.findings[0].message
260
-
261
-
262
- def test_a_declared_root_file_is_accepted() -> None:
263
- verdict = graded_declaring({"root-files": "STATUS.md,DRIFT.md"}, ("STATUS.md", "DRIFT.md"))
174
+ def test_a_declared_asset_dir_admits_an_image_at_any_depth() -> None:
175
+ verdict = graded_declaring(
176
+ {"asset-dirs": "web/public,docs/assets"},
177
+ ("web/public/logo.png", "web/public/deep/er/icon.JPG", "docs/assets/shot/screenshot-1.png"),
178
+ )
264
179
  assert verdict.outcome is Outcome.CLEAN, verdict.render()
265
180
 
266
181
 
267
- def test_a_declaration_admits_only_the_names_it_lists() -> None:
268
- """Without this, the case above also passes against a gate that stops
269
- grading the root as soon as any declaration is present."""
270
- verdict = graded_declaring({"root-files": "STATUS.md"}, ("STATUS.md", "DRIFT.md"))
271
- assert [f.path for f in verdict.findings] == ["DRIFT.md"], verdict.render()
182
+ def test_an_undeclared_image_is_reported_beside_a_declaration() -> None:
183
+ """A gate that stops grading images once any declaration is present must fail this."""
184
+ verdict = graded_declaring(
185
+ {"asset-dirs": "web/public"}, ("web/public/logo.png", "web/other/logo.png", "logo.png")
186
+ )
187
+ assert sorted(f.path for f in verdict.findings) == ["logo.png", "web/other/logo.png"]
272
188
 
273
189
 
274
- def test_a_root_dirs_declaration_does_not_admit_a_root_file() -> None:
275
- verdict = graded_declaring({"root-dirs": "STATUS.md"}, ("STATUS.md",))
276
- assert [f.path for f in verdict.findings] == ["STATUS.md"], verdict.render()
190
+ def test_an_asset_dir_is_a_directory_not_a_name_prefix() -> None:
191
+ verdict = graded_declaring({"asset-dirs": "web/public"}, ("web/public-extra/logo.png",))
192
+ assert [f.path for f in verdict.findings] == ["web/public-extra/logo.png"]
277
193
 
278
194
 
279
- def test_the_root_file_remedy_points_at_the_declaration_not_at_source() -> None:
280
- """The old remedy sent a reader to edit `policy/root_shape.py` in the shared
281
- package. The repository owns the declaration, so the remedy names it."""
282
- finding = graded_declaring({}, ("STATUS.md",)).findings[0]
283
- assert "[controls.compliance.root]" in finding.fix, finding.fix
284
- assert "adl-config.toml" in finding.fix, finding.fix
285
- assert "root_shape.py" not in finding.fix, finding.fix
195
+ def test_a_declared_asset_dir_does_not_admit_a_throwaway_file() -> None:
196
+ verdict = graded_declaring({"asset-dirs": "web/public"}, ("web/public/build.log",))
197
+ assert [f.path for f in verdict.findings] == ["web/public/build.log"]
198
+ assert "throwaway" in verdict.findings[0].message
286
199
 
287
200
 
288
- def test_apps_stays_retired_unless_the_repository_declares_it() -> None:
289
- """The retired-`apps/` rule is active by default, and declaring a DIFFERENT
290
- directory does not switch it off."""
291
- for declarations in ({}, {"root-dirs": "packaging"}, {"root-files": "apps"}):
292
- verdict = graded_declaring(declarations, ("apps/legacy/file.ts",))
293
- assert [f.path for f in verdict.findings] == ["apps/legacy/file.ts"], declarations
294
- assert "apps-retired" in verdict.findings[0].message
201
+ def test_a_trailing_slash_in_a_declared_asset_dir_is_tolerated() -> None:
202
+ verdict = graded_declaring({"asset-dirs": "web/public/"}, ("web/public/logo.png",))
203
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
295
204
 
296
205
 
297
- def test_a_declared_apps_directory_is_not_retired() -> None:
298
- """A declared `apps/` contradicts "apps is retired", so the rule stands down."""
299
- for declarations in ({"root-dirs": "apps"}, {"root-dirs": "packaging, apps"}):
300
- verdict = graded_declaring(declarations, ("apps/legacy/file.ts", "apps/cli/src/main.ts"))
301
- assert verdict.outcome is Outcome.CLEAN, (declarations, verdict.render())
206
+ def test_a_malformed_asset_dir_is_could_not_run() -> None:
207
+ import pytest
302
208
 
209
+ from adl_compliance.core.errors import CouldNotRun
303
210
 
304
- def test_enact_config_is_a_built_in_root_file_beside_adl_config() -> None:
305
- """Owner decision (Tarun, 2026-10-01): an adl-controlled Enact repository
306
- carries BOTH `adl-config.toml` and `enact-config.toml` at its root,
307
- permanently. Neither is declared by hand under `[controls.compliance.root]`."""
308
- verdict = graded_declaring({}, ("adl-config.toml", "enact-config.toml"))
309
- assert verdict.outcome is Outcome.CLEAN, verdict.render()
211
+ for bad in ("/abs", "a/../b", "./x"):
212
+ with pytest.raises(CouldNotRun, match="asset-dirs"):
213
+ graded_declaring({"asset-dirs": bad}, ("a/logo.png",))
310
214
 
311
215
 
312
- def test_an_unknown_root_toml_still_fails_beside_enact_config() -> None:
313
- """Without this, the case above also passes against a gate that admits
314
- every root `.toml`."""
315
- verdict = graded_declaring({}, ("enact-config.toml", "random.toml"))
316
- assert [f.path for f in verdict.findings] == ["random.toml"], verdict.render()
317
-
318
-
319
- # ------------------------------------------------- a tree declared absent
320
- def test_a_rust_tree_declared_absent_contributes_nothing_to_md_sprawl(
321
- tmp_path: pathlib.Path,
322
- ) -> None:
323
- """`rust = ""` (the repository states it has no Rust tree) drops `rust_root`
324
- from the resolved inputs. In `_md_src_trees` that input only ever ADDS a
325
- prefix to the trees whose `src/` Markdown is reported, so absent it
326
- contributes nothing and the literal `agent/` tree still governs. This was a
327
- `KeyError: 'rust_root'` traceback (exit 1, not could-not-run) on any `.md`
328
- path, measured against enact-combined with `--tree rust= --tree frontend=`.
329
- The rule is not switched off: `agent/**/src/*.md` is still reported."""
330
- ctx = Context(root=tmp_path, row=ROW, inputs={"docs_root": tmp_path / "docs"})
331
- findings = tuple(
332
- Placement().graded(
333
- ctx,
334
- FileSet(paths=("notes/overview.md", "backend/crates/x/src/n.md", "agent/crates/x/src/n.md")),
335
- )
336
- )
337
- assert [f.path for f in findings] == ["agent/crates/x/src/n.md"], findings
338
- assert "md-sprawl" in findings[0].message
216
+ def test_the_row_scope_is_every_file_and_excludes_fixtures() -> None:
217
+ assert scope.in_scope(ROW.engine.scope, "logo.png")
218
+ assert scope.in_scope(ROW.engine.scope, "deep/er/notes.log")
219
+ assert not scope.in_scope(ROW.engine.scope, "checks/x/fixtures/logo.png")
220
+ assert ROW.title == "Reject throwaway files and undeclared image placement"
@@ -84,12 +84,13 @@ class PlansLayout(Check):
84
84
  # Three gates held an opinion on this file and they did not agree: LAY-006
85
85
  # allowed `*/LEDGER.md`, DOC-009 required `LEDGER.yaml` and reported the
86
86
  # Markdown form as "legacy", and this list allowed only the YAML. A
87
- # repository could not satisfy all three at once -- enact-m5's conversion
87
+ # repository could not satisfy all three at once -- a managed repository's conversion
88
88
  # failed here the moment it succeeded elsewhere.
89
89
  ROOT_ALLOWED_FILES: ClassVar[tuple[str, ...]] = (
90
90
  "AGENTS.md", "CLAUDE.md", "LEDGER.md", "GAPS-AND-FINDINGS.md",
91
91
  )
92
92
  IGNORED: ClassVar[str] = ".DS_Store"
93
+ KEEP: ClassVar[str] = ".gitkeep"
93
94
 
94
95
  # az<work item id>-<kebab-slug>
95
96
  FEATURE_DIR: ClassVar[re.Pattern[str]] = re.compile(r"^az([0-9]+)-[a-z0-9]+(-[a-z0-9]+)*$")
@@ -245,11 +246,26 @@ class PlansLayout(Check):
245
246
 
246
247
  def _feature_dirs(self, ctx: Context, given: set[str]) -> Iterator[Finding]:
247
248
  features = f"{self._plans(ctx)}/{self.FEATURES}"
248
- for entry in sorted(self._under(features, given)):
249
+ entries = sorted(self._under(features, given))
250
+ # `.gitkeep` is allowed in features/ only until the first Feature folder exists.
251
+ has_feature = any(
252
+ self.FEATURE_DIR.match(entry.rsplit("/", 1)[-1]) and ctx.path(entry).is_dir()
253
+ for entry in entries
254
+ )
255
+ for entry in entries:
249
256
  name = entry.rsplit("/", 1)[-1]
250
257
  if name == self.IGNORED:
251
258
  continue
252
259
 
260
+ if name == self.KEEP and not ctx.path(entry).is_dir():
261
+ if has_feature:
262
+ yield self.report(
263
+ entry,
264
+ "features/ no longer needs .gitkeep: a Feature folder exists",
265
+ f"delete {entry} now that the first Feature exists",
266
+ )
267
+ continue
268
+
253
269
  if not ctx.path(entry).is_dir():
254
270
  yield self.report(
255
271
  entry,
@@ -165,6 +165,55 @@ def test_a_loose_file_in_features_is_reported(tmp_path: pathlib.Path) -> None:
165
165
  assert any("one directory per Feature" in f.message for f in verdict.findings)
166
166
 
167
167
 
168
+ # LAY-009: an empty features/ may hold `.gitkeep`; the first Feature folder retires it.
169
+
170
+ def _plans_files(*extra: str) -> dict[str, str]:
171
+ return {"plans/AGENTS.md": "x", **{path: "" for path in extra}}
172
+
173
+
174
+ def test_a_gitkeep_alone_in_an_empty_features_directory_is_silent(
175
+ tmp_path: pathlib.Path,
176
+ ) -> None:
177
+ verdict = graded_directly(build(tmp_path, _plans_files("plans/features/.gitkeep")))
178
+ assert not any(".gitkeep" in f.path for f in verdict.findings), verdict.render()
179
+ assert not any("one directory per Feature" in f.message for f in verdict.findings)
180
+
181
+
182
+ def test_a_gitkeep_beside_a_feature_is_reported_and_names_the_deletion(
183
+ tmp_path: pathlib.Path,
184
+ ) -> None:
185
+ files = {
186
+ "plans/features/az1-x/spec.md": FEATURE.format(work_item=1),
187
+ "plans/features/.gitkeep": "",
188
+ }
189
+ verdict = graded_directly(build(tmp_path, {"plans/AGENTS.md": "x", **files}))
190
+ kept = [f for f in verdict.findings if f.path == "plans/features/.gitkeep"]
191
+ assert len(kept) == 1, verdict.render()
192
+ assert "delete plans/features/.gitkeep" in kept[0].fix
193
+ assert "first Feature" in kept[0].fix
194
+
195
+
196
+ def test_any_other_loose_file_is_still_reported_beside_a_gitkeep(
197
+ tmp_path: pathlib.Path,
198
+ ) -> None:
199
+ verdict = graded_directly(
200
+ build(tmp_path, _plans_files("plans/features/.gitkeep", "plans/features/stray.md"))
201
+ )
202
+ paths = {f.path for f in verdict.findings}
203
+ assert "plans/features/stray.md" in paths
204
+ assert "plans/features/.gitkeep" not in paths
205
+
206
+
207
+ def test_a_gitkeep_is_only_the_features_directorys_exemption(tmp_path: pathlib.Path) -> None:
208
+ """Strictness: the allowance is for `features/` alone, not a Feature folder."""
209
+ files = {
210
+ "plans/features/az1-x/spec.md": FEATURE.format(work_item=1),
211
+ "plans/features/az1-x/.gitkeep": "",
212
+ }
213
+ verdict = graded_directly(build(tmp_path, {"plans/AGENTS.md": "x", **files}))
214
+ assert any(f.path == "plans/features/az1-x/.gitkeep" for f in verdict.findings)
215
+
216
+
168
217
  def test_a_feature_directory_not_named_az_id_is_reported(tmp_path: pathlib.Path) -> None:
169
218
  verdict = graded_directly(
170
219
  build(tmp_path, {"plans/features/coverage/spec.md": FEATURE.format(work_item=1)})
@@ -1,4 +1,4 @@
1
- """LAY-011 — every local link in a Markdown page under docs/.plans/.specs
1
+ """LAY-011 — every local link in a Markdown page under docs/plans
2
2
  resolves, exactly (case included), and no page still points at a retired
3
3
  docs/strategy/ path.
4
4
 
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: "Links"
3
+ description: "Three links on this page do not resolve"
4
+ document_type: architecture
5
+ status: canonical
6
+ last_update: "2026-07-27T00:00:00Z"
7
+ reason: "fixture"
8
+ ---
9
+
10
+ [Sibling](./route-target.md)
11
+
12
+ [Published route](/architecture/guides/route-target/)
13
+
14
+ [Completed plan](../../../.plans/c_done/26-07-27-history.md)
15
+
16
+ [Missing](./does-not-exist.md)
17
+
18
+ [Wrong case](../../readme.md)
19
+
20
+ Retired path: `docs/strategy/old-page.md`.
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: "Route target"
3
+ description: "Existing target for a published route reference"
4
+ document_type: architecture
5
+ status: canonical
6
+ last_update: "2026-07-27T00:00:00Z"
7
+ reason: "fixture"
8
+ ---
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: "Links"
3
+ description: "Every link on this page resolves"
4
+ document_type: architecture
5
+ status: canonical
6
+ last_update: "2026-07-27T00:00:00Z"
7
+ reason: "fixture"
8
+ ---
9
+
10
+ [Sibling](./route-target.md)
11
+
12
+ [Published route](/architecture/guides/route-target/)
13
+
14
+ [Exact case](../../README.md)
15
+
16
+ [Completed plan](../../../.plans/c_done/26-07-27-history.md)
17
+
18
+ ```
19
+ [Inside a fence](./does-not-exist.md)
20
+ ```
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: "Route target"
3
+ description: "Existing target for a published route reference"
4
+ document_type: architecture
5
+ status: canonical
6
+ last_update: "2026-07-27T00:00:00Z"
7
+ reason: "fixture"
8
+ ---
@@ -72,7 +72,7 @@ def test_broken_links_are_reported_exactly() -> None:
72
72
 
73
73
  rendered = verdict.render()
74
74
  assert (
75
- "docs/architecture/product/links.md:16: missing local reference "
75
+ "docs/architecture/guides/links.md:16: missing local reference "
76
76
  "./does-not-exist.md" in rendered
77
77
  ), rendered
78
78
 
@@ -81,7 +81,7 @@ def test_broken_links_are_reported_exactly() -> None:
81
81
  # a check that tested existence alone would call this link resolved. It is
82
82
  # wrong by case and by nothing else.
83
83
  assert (
84
- "docs/architecture/product/links.md:18: missing local reference "
84
+ "docs/architecture/guides/links.md:18: missing local reference "
85
85
  "../../readme.md" in rendered
86
86
  ), rendered
87
87
 
@@ -104,7 +104,7 @@ def test_a_page_outside_the_given_set_is_not_graded() -> None:
104
104
  to grade."""
105
105
  verdict = graded(
106
106
  CORPORA / "link-targets-broken",
107
- given=("docs/architecture/product/route-target.md",),
107
+ given=("docs/architecture/guides/route-target.md",),
108
108
  )
109
109
  assert verdict.outcome is Outcome.CLEAN, verdict.render()
110
110
 
@@ -118,7 +118,7 @@ def test_a_completed_plan_resolves_as_a_target_though_excluded_as_a_source() ->
118
118
  missing one."""
119
119
  verdict = graded(
120
120
  CORPORA / "link-targets-clean",
121
- given=("docs/architecture/product/links.md", "docs/architecture/product/route-target.md"),
121
+ given=("docs/architecture/guides/links.md", "docs/architecture/guides/route-target.md"),
122
122
  )
123
123
  assert verdict.outcome is Outcome.CLEAN, verdict.render()
124
124
 
@@ -58,13 +58,13 @@ from typing import ClassVar
58
58
 
59
59
  from ....core.check import Check
60
60
  from ....core.context import Context, FileSet
61
+ from ....core.declarations import declared_root_files
61
62
  from ....core.finding import Report
62
63
  from ....policy.root_shape import (
63
64
  ALLOWED_ROOT_DIRS,
64
65
  ALLOWED_ROOT_FILES,
65
66
  REQUIRED_ROOT_FILES,
66
67
  allowed_root_dirs,
67
- allowed_root_files,
68
68
  )
69
69
 
70
70
 
@@ -100,7 +100,7 @@ class RootClosedSet(Check):
100
100
 
101
101
  # PERMITTED at the root, same as the files above: present is fine, absent
102
102
  # is fine. Several entries here name trees no longer in use anywhere
103
- # (`.code-review-graph_old`, `.plans`, `.specs`) and they cost nothing while
103
+ # (`.code-review-graph_old`, `.plans`) and they cost nothing while
104
104
  # they only permit; under the old manifest reading, each was a finding.
105
105
  #
106
106
  # The CENTRAL default. The effective set is
@@ -135,7 +135,7 @@ class RootClosedSet(Check):
135
135
  root_files.add(path)
136
136
 
137
137
  allowed_dirs = allowed_root_dirs(ctx.declarations)
138
- allowed_files = allowed_root_files(ctx.declarations)
138
+ allowed_files = ALLOWED_ROOT_FILES | declared_root_files(ctx.declarations)
139
139
  for name in sorted(dirs):
140
140
  if name not in allowed_dirs:
141
141
  yield self.report(
@@ -0,0 +1 @@
1
+ root-layout corpus: content under a declared top-level directory.
@@ -41,8 +41,15 @@ def clean_set() -> tuple[str, ...]:
41
41
  )
42
42
 
43
43
 
44
+ # PBI 599/601: the corpus declares its dirs (`root-dirs`) and files (`root-files`) as a real repository does.
45
+ CORPUS_DIRS = {
46
+ "root-dirs": "backend,dev,docker,frontend,infra,.docs,.notes,.donors",
47
+ "root-files": "CLAUDE-INVARIANTS.md,INDEPENDENT-VERIFICATION.md",
48
+ }
49
+
50
+
44
51
  def verdict_for(given: tuple[str, ...]) -> Verdict:
45
- ctx = Context(root=CORPUS, row=ROW)
52
+ ctx = Context(root=CORPUS, row=ROW, declarations=CORPUS_DIRS)
46
53
  reported = tuple(RootClosedSet().run(ctx, FileSet(paths=given)))
47
54
  findings = tuple(r for r in reported if isinstance(r, Finding))
48
55
  advisories = tuple(r for r in reported if isinstance(r, Advisory))