@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.12 → 0.1.26

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 (405) hide show
  1. package/bin/adl +2 -2
  2. package/bin/git +16 -17
  3. package/bin/git-ado +25 -24
  4. package/bin/git-defaults +173 -0
  5. package/bin/health-check +8 -1
  6. package/bin/kimi-hooks +1 -1
  7. package/bin/lib/adl-package.sh +257 -0
  8. package/bin/lib/fleet-host.sh +8 -5
  9. package/bin/reconcile +482 -232
  10. package/bin/reconcile-host +30 -20
  11. package/bin/setup +20 -17
  12. package/bin/shell-path-policy +15 -5
  13. package/bin/sync-and-reconcile +62 -64
  14. package/bin/validate-manifest +33 -9
  15. package/package.json +2 -10
  16. package/packages/agent-hooks/README.md +14 -9
  17. package/packages/agent-hooks/assets/hooks.json +0 -20
  18. package/packages/agent-hooks/src/cli/args.ts +1 -1
  19. package/packages/agent-hooks/src/cli/doctor.ts +6 -58
  20. package/packages/agent-hooks/src/deploy/deploy.ts +12 -41
  21. package/packages/agent-hooks/src/deploy/doctor.ts +8 -11
  22. package/packages/agent-hooks/src/deploy/index.ts +14 -14
  23. package/packages/agent-hooks/src/deploy/json-merge.ts +7 -8
  24. package/packages/agent-hooks/src/deploy/legacy-lock.ts +2 -2
  25. package/packages/agent-hooks/src/deploy/list.ts +1 -1
  26. package/packages/agent-hooks/src/deploy/lock.ts +31 -12
  27. package/packages/agent-hooks/src/deploy/profile.ts +3 -4
  28. package/packages/agent-hooks/src/deploy/source.ts +1 -1
  29. package/packages/agent-hooks/src/gating/config.ts +10 -24
  30. package/packages/agent-hooks/src/gating/root.ts +3 -3
  31. package/packages/agent-hooks/src/hooks/briefing/handler.ts +2 -2
  32. package/packages/agent-hooks/src/hooks/briefing/index.ts +27 -287
  33. package/packages/agent-hooks/src/hooks/contract.ts +17 -17
  34. package/packages/agent-hooks/src/hooks/crg/background.ts +27 -0
  35. package/packages/agent-hooks/src/hooks/crg/index.ts +7 -6
  36. package/packages/agent-hooks/src/hooks/log.ts +8 -10
  37. package/packages/agent-hooks/src/hooks/user-prompt-submit/match.ts +1 -1
  38. package/packages/agent-hooks/src/hooks/user-prompt-submit/tokenizer.ts +1 -1
  39. package/packages/agent-hooks/src/hooks/user-prompt-submit/vector.ts +6 -5
  40. package/packages/agent-hooks/src/hosts/registry.ts +1 -1
  41. package/packages/agent-hooks/src/log/adl-dir.ts +44 -0
  42. package/packages/agent-hooks/src/log/writer.ts +16 -2
  43. package/packages/agent-hooks/src/main.ts +7 -41
  44. package/packages/agent-hooks/src/pre-tool-use/handler.ts +71 -82
  45. package/packages/agent-hooks/src/rules/engine.ts +13 -17
  46. package/packages/agent-hooks/src/rules/impl/adl-build-run-denied.ts +3 -3
  47. package/packages/agent-hooks/src/rules/impl/adl-git-credential-bypass.ts +12 -14
  48. package/packages/agent-hooks/src/rules/impl/adl-git-identity.ts +28 -28
  49. package/packages/agent-hooks/src/rules/impl/adl-git-push-policy.ts +8 -9
  50. package/packages/agent-hooks/src/rules/impl/adl-human-azdo-bypass.ts +56 -59
  51. package/packages/agent-hooks/src/rules/impl/adl-identity.ts +16 -23
  52. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +19 -16
  53. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +2 -2
  54. package/packages/agent-hooks/src/rules/impl/adl-pr-review-role.ts +9 -12
  55. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +5 -7
  56. package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +411 -0
  57. package/packages/agent-hooks/src/rules/impl/adl-token-minting-role.ts +4 -5
  58. package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +6 -7
  59. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +77 -70
  60. package/packages/agent-hooks/src/rules/impl/deny-allowlist.ts +159 -0
  61. package/packages/agent-hooks/src/rules/impl/git-helpers.ts +21 -23
  62. package/packages/agent-hooks/src/rules/impl/git-worktree.ts +14 -28
  63. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +3 -3
  64. package/packages/agent-hooks/src/rules/impl/messages.ts +1 -1
  65. package/packages/agent-hooks/src/rules/impl/pr-completion.ts +1 -1
  66. package/packages/agent-hooks/src/rules/impl/tool-azure-devops.ts +35 -46
  67. package/packages/agent-hooks/src/rules/impl/tool-find.ts +34 -7
  68. package/packages/agent-hooks/src/rules/impl/tool-grep.ts +34 -12
  69. package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +52 -9
  70. package/packages/agent-hooks/src/rules/ports.ts +5 -7
  71. package/packages/agent-hooks/src/rules/registry.ts +4 -6
  72. package/packages/agent-hooks/src/rules/shell/git-identity.ts +86 -101
  73. package/packages/agent-hooks/src/rules/shell/git-internals.ts +1 -1
  74. package/packages/agent-hooks/src/rules/shell/invocation.ts +24 -118
  75. package/packages/agent-hooks/src/rules/shell/piped-script.ts +1 -1
  76. package/packages/agent-hooks/src/rules/shell/prefixes.ts +1 -1
  77. package/packages/agent-hooks/src/rules/shell/protected-carrier-writes.ts +11 -10
  78. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +26 -26
  79. package/packages/agent-hooks/src/rules/shell/substitution.ts +1 -1
  80. package/packages/agent-hooks/src/rules/shell/xargs.ts +2 -2
  81. package/packages/agent-hooks/src/rules/spec.ts +2 -2
  82. package/packages/agent-settings/README.md +287 -0
  83. package/packages/agent-settings/assets/GLOBAL-AGENTS.md +14 -16
  84. package/packages/agent-settings/assets/agents/architect.toml +2 -0
  85. package/packages/agent-settings/assets/agents/code-reviewer.toml +2 -0
  86. package/packages/agent-settings/assets/agents/executor.toml +2 -0
  87. package/packages/agent-settings/assets/agents/explore.toml +2 -0
  88. package/packages/agent-settings/assets/agents/planner.toml +2 -0
  89. package/packages/agent-settings/assets/mcp.json +0 -4
  90. package/packages/agent-settings/deploy-profile.json +139 -0
  91. package/packages/agent-settings/deploy-profile.schema.json +147 -0
  92. package/packages/agent-settings/managed/README.md +29 -30
  93. package/packages/agent-settings/managed/codex-requirements.toml +9 -9
  94. package/packages/agent-settings/package.json +20 -0
  95. package/packages/agent-settings/src/deploy/agent-render.ts +2 -2
  96. package/packages/agent-settings/src/deploy/agent-toml.ts +1 -1
  97. package/packages/agent-settings/src/deploy/agents-deploy.ts +17 -3
  98. package/packages/agent-settings/src/deploy/doctor.ts +3 -83
  99. package/packages/agent-settings/src/deploy/index.ts +6 -33
  100. package/packages/agent-settings/src/deploy/legacy-lock.ts +6 -6
  101. package/packages/agent-settings/src/deploy/list.ts +5 -19
  102. package/packages/agent-settings/src/deploy/lock.ts +8 -18
  103. package/packages/agent-settings/src/deploy/mcp-deploy.ts +10 -5
  104. package/packages/agent-settings/src/deploy/mcp-merge.ts +22 -3
  105. package/packages/agent-settings/src/deploy/mcp-source.ts +1 -1
  106. package/packages/agent-settings/src/deploy/profile.ts +11 -35
  107. package/packages/agent-settings/src/deploy/retire-lean-ctx.ts +184 -0
  108. package/packages/agent-settings/src/deploy/settings.ts +17 -22
  109. package/packages/agent-settings/src/global-surface-sweep.ts +0 -2
  110. package/packages/agent-settings/src/kimi-hooks.ts +339 -192
  111. package/packages/agent-settings/src/opencode-hooks.ts +1 -1
  112. package/packages/agent-settings/tsconfig.json +23 -0
  113. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +120 -0
  114. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +276 -0
  115. package/packages/agent-skills/assets/debug-issue/SKILL.md +8 -7
  116. package/packages/agent-skills/assets/explore-codebase/SKILL.md +13 -13
  117. package/packages/agent-skills/assets/refactor-safely/SKILL.md +12 -11
  118. package/packages/agent-skills/assets/review-changes/SKILL.md +7 -7
  119. package/packages/agent-skills/assets/testing/references/e2e.md +37 -0
  120. package/packages/agent-skills/assets/testing/references/integration.md +37 -0
  121. package/packages/agent-skills/assets/testing/references/unit.md +32 -0
  122. package/packages/agent-skills/dist/deploy.d.ts +2 -0
  123. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  124. package/packages/agent-skills/dist/deploy.js +27 -1
  125. package/packages/agent-skills/dist/deploy.js.map +1 -1
  126. package/packages/agent-skills/dist/doctor.d.ts +0 -2
  127. package/packages/agent-skills/dist/doctor.d.ts.map +1 -1
  128. package/packages/agent-skills/dist/doctor.js +1 -1
  129. package/packages/agent-skills/dist/doctor.js.map +1 -1
  130. package/packages/agent-skills/dist/fs-utils.d.ts +0 -1
  131. package/packages/agent-skills/dist/fs-utils.d.ts.map +1 -1
  132. package/packages/agent-skills/dist/fs-utils.js +1 -1
  133. package/packages/agent-skills/dist/fs-utils.js.map +1 -1
  134. package/packages/agent-skills/src/deploy.ts +29 -1
  135. package/packages/agent-skills/src/doctor.ts +1 -1
  136. package/packages/agent-skills/src/fs-utils.ts +1 -1
  137. package/packages/azdo/README.md +10 -9
  138. package/packages/azdo/package.json +1 -3
  139. package/packages/azdo/src/azdo-pl.ts +2 -54
  140. package/packages/azdo/src/azdo-pr-review.ts +4 -4
  141. package/packages/azdo/src/azdo-repo-doctor.ts +101 -52
  142. package/packages/azdo/src/azdo-wit.ts +41 -2
  143. package/packages/azdo/src/lib/agent-identity.ts +12 -14
  144. package/packages/azdo/src/lib/agent-token.ts +2 -2
  145. package/packages/azdo/src/lib/client.ts +0 -5
  146. package/packages/azdo/src/lib/graph-identity.ts +4 -4
  147. package/packages/azdo/src/workflow-rules-matrix.csv +85 -0
  148. package/packages/azdo/src/workflow-rules.ts +128 -0
  149. package/packages/fleet/manifest.json +35 -11
  150. package/packages/fleet/manifest.schema.json +62 -10
  151. package/packages/fleet/src/default-agent-file.ts +3 -3
  152. package/packages/fleet/src/install-adl-package.ts +73 -15
  153. package/packages/fleet/src/install-embedding-model.ts +13 -13
  154. package/packages/fleet/src/managed-settings.ts +7 -7
  155. package/packages/fleet/src/repo-maintain.ts +91 -55
  156. package/packages/fleet/src/scratch-reap.ts +5 -5
  157. package/packages/fleet/src/vscode-settings.ts +5 -5
  158. package/packages/repo-controls/README.md +141 -43
  159. package/packages/repo-controls/assets/promote.ts +4 -4
  160. package/packages/repo-controls/assets/reconcile-guard.sh +10 -46
  161. package/packages/repo-controls/assets/version-bump.sh +31 -84
  162. package/packages/repo-controls/bunfig.toml +6 -7
  163. package/packages/repo-controls/package/adl-compliance/AGENTS.md +5 -6
  164. package/packages/repo-controls/package/adl-compliance/conftest.py +3 -1
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +4 -16
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +9 -2
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/notes.md +1 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +11 -0
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/__init__.py +7 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/check.py +266 -0
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.drafts/c.md +1 -0
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.drafts/d.md +1 -0
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.hidden.md +1 -0
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/a.md +1 -0
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/b.md +1 -0
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/data.csv +1 -0
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/diagram.png +1 -0
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/mock.png +1 -0
  179. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/sub/nested.md +1 -0
  180. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs.md +1 -0
  181. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/grants.yaml +4 -0
  182. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p1.md +1 -0
  183. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p2.md +1 -0
  184. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p3.md +1 -0
  185. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p4.md +1 -0
  186. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p5.md +1 -0
  187. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/build.md +1 -0
  188. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/overview.md +1 -0
  189. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/install.md +1 -0
  190. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/start.md +1 -0
  191. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/grants.yaml +4 -0
  192. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-001-tenancy.md +1 -0
  193. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-002-carrier.md +1 -0
  194. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-003-cli.md +1 -0
  195. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/archive/adr-000-old.md +1 -0
  196. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/archive/adr-000-older.md +1 -0
  197. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/a.md +1 -0
  198. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/b.md +1 -0
  199. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/c.md +1 -0
  200. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/grants.yaml +4 -0
  201. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-01.md +1 -0
  202. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-02.md +1 -0
  203. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-03.md +1 -0
  204. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-04.md +1 -0
  205. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-05.md +1 -0
  206. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-06.md +1 -0
  207. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-07.md +1 -0
  208. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-08.md +1 -0
  209. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-09.md +1 -0
  210. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-10.md +1 -0
  211. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-11.md +1 -0
  212. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-12.md +1 -0
  213. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-13.md +1 -0
  214. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-14.md +1 -0
  215. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-15.md +1 -0
  216. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-16.md +1 -0
  217. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/grants.yaml +4 -0
  218. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/a.md +1 -0
  219. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/b.md +1 -0
  220. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/c.md +1 -0
  221. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/grants.yaml +4 -0
  222. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/a.md +1 -0
  223. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/b.md +1 -0
  224. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/c.md +1 -0
  225. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/grants.yaml +4 -0
  226. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/a.md +1 -0
  227. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/b.md +1 -0
  228. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/c.md +1 -0
  229. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/d.md +1 -0
  230. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/e.md +1 -0
  231. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/grants.yaml +4 -0
  232. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/a.md +1 -0
  233. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/b.md +1 -0
  234. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/c.md +1 -0
  235. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/short/only.md +1 -0
  236. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/grants.yaml +4 -0
  237. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/a.md +1 -0
  238. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/b.md +1 -0
  239. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/c.md +1 -0
  240. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/d.md +1 -0
  241. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/a.md +1 -0
  242. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/b.md +1 -0
  243. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/grants.yaml +6 -0
  244. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/a.md +1 -0
  245. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/b.md +1 -0
  246. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/c.md +1 -0
  247. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/grants.yaml +3 -0
  248. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/test_check.py +345 -0
  249. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +18 -5
  250. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +35 -0
  251. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/__init__.py +7 -0
  252. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/check.py +91 -0
  253. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/AGENTS.md +3 -0
  254. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/CLAUDE.md +3 -0
  255. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/-leading-hyphen.md +3 -0
  256. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/AGENTS.md +3 -0
  257. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/double--hyphen.md +3 -0
  258. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/getting started.md +3 -0
  259. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/nested/page-here.md +3 -0
  260. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/trailing-hyphen-.md +3 -0
  261. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +4 -0
  262. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/user_profile.md +3 -0
  263. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/UI-CONTRACT.png +2 -0
  264. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/diagram.png +1 -0
  265. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/index.md +4 -0
  266. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/notes.md +3 -0
  267. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/reference.html +1 -0
  268. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/2026-10-experiment.md +3 -0
  269. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +6 -0
  270. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs.md +5 -0
  271. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +194 -0
  272. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +14 -2
  273. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +5 -5
  274. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-shipped-register-legacy/docs/compliance/index.md +10 -0
  275. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-shipped-register-type/docs/compliance/index.md +10 -0
  276. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +58 -6
  277. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +0 -1
  278. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/check.py +64 -17
  279. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +60 -9
  280. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-clean/docs/notes.md +17 -0
  281. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-no-lines/docs/notes.md +13 -0
  282. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs/architecture/demo.md +0 -1
  283. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs-okf-profiles.yaml +0 -1
  284. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/no-type.md +9 -0
  285. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-experiment.md +10 -0
  286. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-research.md +10 -0
  287. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs-okf-profiles.yaml +5 -0
  288. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs/architecture/bad-status.md +0 -1
  289. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs-okf-profiles.yaml +0 -1
  290. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/experiment-ok.md +15 -0
  291. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +15 -0
  292. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +16 -0
  293. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +11 -0
  294. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +11 -0
  295. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-last-update.md +11 -0
  296. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-sources.md +10 -0
  297. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +11 -0
  298. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +11 -0
  299. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +16 -0
  300. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +14 -0
  301. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/architecture/created-date.md +21 -0
  302. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/architecture/research-shaped.md +19 -0
  303. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/operations/runbook.md +26 -0
  304. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +228 -2
  305. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +5 -0
  306. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +11 -2
  307. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/__init__.py +5 -3
  308. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +160 -44
  309. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +0 -2
  310. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/frontend/packages/desktop/src/a.ts +1 -0
  311. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/frontend/packages/desktop/src/b.ts +1 -0
  312. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/grants.yaml +3 -0
  313. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +0 -2
  314. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
  315. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +5 -0
  316. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/a.ts +1 -0
  317. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/b.ts +1 -0
  318. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/c.ts +1 -0
  319. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +4 -0
  320. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
  321. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +5 -0
  322. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +0 -2
  323. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
  324. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/grants.yaml +3 -0
  325. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/register.py +127 -0
  326. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +265 -29
  327. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +75 -2
  328. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +194 -0
  329. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +56 -10
  330. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +256 -0
  331. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +40 -12
  332. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +108 -0
  333. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +1 -1
  334. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +2 -3
  335. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +23 -6
  336. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +76 -0
  337. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +58 -17
  338. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +161 -0
  339. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +35 -3
  340. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +108 -1
  341. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +281 -61
  342. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/absolute-path.yaml +3 -0
  343. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/backslash-path.yaml +3 -0
  344. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/baseline-is-a-list.yaml +2 -0
  345. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/boolean-size.yaml +3 -0
  346. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/dot-segment.yaml +3 -0
  347. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/duplicate-path.yaml +4 -0
  348. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-file.yaml +0 -0
  349. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-segment.yaml +3 -0
  350. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/float-size.yaml +3 -0
  351. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/negative-size.yaml +3 -0
  352. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-over-limit-baseline.yaml +1 -0
  353. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-schema-version.yaml +1 -0
  354. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/non-string-path.yaml +3 -0
  355. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/not-yaml.yaml +2 -0
  356. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/null-size.yaml +3 -0
  357. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/parent-segment.yaml +3 -0
  358. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/string-size.yaml +3 -0
  359. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/top-level-is-a-list.yaml +2 -0
  360. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/trailing-slash.yaml +3 -0
  361. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/unknown-key.yaml +3 -0
  362. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/wrong-schema-version.yaml +2 -0
  363. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/zero-size.yaml +3 -0
  364. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/valid.yaml +4 -0
  365. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +758 -45
  366. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/check.py +4 -3
  367. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/test_check.py +2 -2
  368. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +490 -0
  369. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +368 -55
  370. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +4 -2
  371. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +523 -0
  372. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +1179 -0
  373. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +122 -3
  374. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +33 -1
  375. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +0 -11
  376. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/context.py +16 -1
  377. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -2
  378. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +12 -1
  379. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +133 -147
  380. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +133 -147
  381. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  382. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +52 -10
  383. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +23 -4
  384. package/packages/repo-controls/package.json +2 -9
  385. package/packages/repo-controls/scripts/hooks/post-merge +13 -0
  386. package/packages/repo-controls/scripts/hooks/pre-push +3 -3
  387. package/packages/repo-controls/src/azdo-push.ts +64 -36
  388. package/packages/repo-controls/src/azure-devops-governance.ts +7 -9
  389. package/packages/repo-controls/src/azure-devops-remote.ts +1 -1
  390. package/packages/repo-controls/src/cli.ts +29 -8
  391. package/packages/repo-controls/src/compliance-config.ts +410 -2
  392. package/packages/repo-controls/src/config.ts +133 -60
  393. package/packages/repo-controls/src/constants.ts +13 -27
  394. package/packages/repo-controls/src/coverage-ratchet.ts +4 -4
  395. package/packages/repo-controls/src/doctor.ts +46 -19
  396. package/packages/repo-controls/src/gitignore-block.ts +5 -29
  397. package/packages/repo-controls/src/hook-log.ts +1 -2
  398. package/packages/repo-controls/src/hook.ts +201 -45
  399. package/packages/repo-controls/src/install.ts +14 -8
  400. package/packages/repo-controls/src/integrity.ts +3 -3
  401. package/packages/repo-controls/src/lfs.ts +111 -0
  402. package/packages/repo-controls/src/promote.ts +8 -8
  403. package/packages/repo-controls/src/runtime.ts +9 -1
  404. package/packages/repo-controls/src/workflows-drift.ts +4 -5
  405. package/packages/repo-controls/src/worktree-guard.ts +3 -3
@@ -3,19 +3,32 @@
3
3
  test.sh builds one fixture inline (`mktemp -d`, real `git init`/`add`/`commit`
4
4
  calls, `head -c 1048577 /dev/zero`, `git lfs install --local --force`) and
5
5
  walks it through a sequence of states, asserting the gate's verdict at each
6
- one. Ported here as separate COMMITTED fixture directories, one per state —
7
- the same shift every other ported check.sh/test.sh pair in this package
8
- already made, and the one this task calls out `coverage.backend-ratchet`
9
- (`COV-002`) as its concrete precedent for. Nothing here invents a scenario
10
- test.sh does not have; each fixture below is named for, and traced back to,
11
- one specific `run_gate`/`run_gate_tree` call in test.sh.
12
-
13
- check.sh reads `:path`/`HEAD:path` (index or `HEAD`, by `ADL_GATE_PHASE`)
14
- and this port reads the WORKTREE file instead — see `check.py`'s module
15
- docstring for why (K-41: no check calls git; K-43: no phase). Concretely,
16
- that collapses two of test.sh's cases into one fixture each here:
17
-
18
- test.sh case -> fixture
6
+ one. This port builds one corpus per state in `tmp_path`, at test time.
7
+
8
+ WHY NOTHING IS COMMITTED. The previous port committed one directory per state.
9
+ Those directories held `.gitattributes` lines of `*.bin filter=lfs ...`, and a
10
+ committed `.gitattributes` also governs the OUTER repository: git-lfs converted
11
+ the raw 1,048,577-byte `attribute-without-conversion/large file.bin` into a
12
+ 132-byte pointer at commit time, byte-identical to `clean/`. On a fresh clone
13
+ exactly one of the `clean` / `attribute-without-conversion` tests then failed,
14
+ depending on whether the checkout smudged LFS. A corpus generated under
15
+ `tmp_path` lies outside the repository, so no outer filter can touch it.
16
+
17
+ Every case asserts its PRECONDITION (size, first line, which files exist)
18
+ before it is graded, and that the runner graded at least one file, so a corpus
19
+ that is not what its name says fails loudly instead of passing for the wrong
20
+ reason. Dispatch is the REAL registry -> `CHECKS` -> `runner.run` path: nothing
21
+ here patches `runner.lookup`.
22
+
23
+ check.sh reads `:path`/`HEAD:path` (index or `HEAD`, by `ADL_GATE_PHASE`).
24
+ This port reads the committed size the CLI supplies in `Context.committed_sizes`
25
+ where there is one, and otherwise the WORKTREE file -- see `check.py`'s module
26
+ docstring (K-41: no check calls git, the CLI measures; K-43: no phase). The
27
+ corpora below carry no committed sizes, so they all take the worktree path, and
28
+ the section at the bottom covers the committed one. Concretely, that collapses
29
+ two of test.sh's cases into one corpus each here:
30
+
31
+ test.sh case -> corpus
19
32
  ---------------------------------------------------------------------
20
33
  pre-commit: raw blob staged, no .gitattributes missing-filter
21
34
  pre-commit: .gitattributes matches, blob still raw attribute-without-conversion
@@ -24,43 +37,154 @@ that collapses two of test.sh's cases into one fixture each here:
24
37
  qua_deploy: payload.dat, large, unmatched extension unmatched-extension
25
38
  pre-commit: small.txt, silent clean (txt)
26
39
 
27
- `clean/` alone therefore stands in for THREE of test.sh's assertions (the
40
+ `clean` alone therefore stands in for THREE of test.sh's assertions (the
28
41
  converted-pointer pass at both phases, plus the untouched small file), which
29
42
  is exact rather than a simplification: with no phase in this port, "passes at
30
43
  pre-commit" and "passes at qua_deploy" are the same claim about the same
31
44
  file.
45
+
46
+ A real 132-byte pointer is at most the threshold, so `check.py` skips it on
47
+ size alone and `clean` never reaches the pointer comparison. That comparison is
48
+ reached only by a file OVER the threshold whose first line is the pointer
49
+ header; the `pointer header over the threshold` cases below are the ones that
50
+ exercise it, and they differ from `attribute-without-conversion` only in that
51
+ first line.
32
52
  """
33
53
 
34
54
  from __future__ import annotations
35
55
 
36
- from helpers import corpus, corpus_files
56
+ import hashlib
57
+ import re
58
+ from collections.abc import Mapping, Sequence
59
+ from pathlib import Path
60
+
61
+ import pytest
62
+ from fixture_trees import fixture_trees
63
+ from helpers import corpus_files
37
64
 
38
65
  from adl_compliance import runner
66
+ from adl_compliance.checks import lookup
39
67
  from adl_compliance.checks.lfs.integrity import Integrity
40
- from adl_compliance.core.verdict import Outcome
68
+ from adl_compliance.core import scope
69
+ from adl_compliance.core.verdict import Outcome, Verdict
41
70
  from adl_compliance.policy.registry import load
42
71
 
43
- import pytest
44
-
45
- FIXTURES = corpus("lfs", "integrity")
46
72
  ROW = load().by_id("LFS-001")
47
73
 
74
+ AT_THRESHOLD = 1_048_576
75
+ OVER_THRESHOLD = 1_048_577
76
+ RAW_OVER = b"\0" * OVER_THRESHOLD
77
+ RAW_AT = b"\0" * AT_THRESHOLD
78
+
79
+ LFS_ATTRIBUTES = "*.bin filter=lfs diff=lfs merge=lfs -text\n"
80
+ POINTER_HEADER = "version https://git-lfs.github.com/spec/v1"
81
+ # What a real pointer says about the 1,048,577-byte blob it stands in for.
82
+ POINTER_TEXT = (
83
+ f"{POINTER_HEADER}\n"
84
+ f"oid sha256:{hashlib.sha256(RAW_OVER).hexdigest()}\n"
85
+ f"size {OVER_THRESHOLD}\n"
86
+ )
87
+ POINTER = POINTER_TEXT.encode()
88
+ # The same header followed by zero bytes out to OVER_THRESHOLD: a file the size
89
+ # check does not skip whose first line IS the pointer header.
90
+ POINTER_HEADER_OVER = POINTER + b"\0" * (OVER_THRESHOLD - len(POINTER))
91
+
92
+
93
+ # ------------------------------------------------------------ building a corpus
94
+
95
+
96
+ def build(tmp_path: Path, files: Mapping[str, bytes], gitattributes: str | None = None) -> Path:
97
+ root = tmp_path / "corpus"
98
+ root.mkdir()
99
+ if gitattributes is not None:
100
+ (root / ".gitattributes").write_text(gitattributes)
101
+ for rel, content in files.items():
102
+ (root / rel).write_bytes(content)
103
+ return root
104
+
105
+
106
+ def first_line(root: Path, rel: str) -> str:
107
+ with (root / rel).open("rb") as handle:
108
+ prefix = handle.read(4096)
109
+ return prefix.split(b"\n", 1)[0].decode("utf-8", errors="replace").rstrip("\r")
110
+
111
+
112
+ def expect_raw_blob(root: Path, rel: str, size: int) -> None:
113
+ """Precondition: `rel` is exactly `size` raw bytes, not a pointer."""
114
+ assert (root / rel).stat().st_size == size, f"{rel}: expected {size} bytes"
115
+ assert first_line(root, rel) != POINTER_HEADER, f"{rel} must not start with the pointer header"
116
+
117
+
118
+ def expect_real_pointer(root: Path, rel: str) -> None:
119
+ """Precondition: `rel` is a genuine 3-line LFS pointer, well under the threshold."""
120
+ text = (root / rel).read_text()
121
+ lines = text.splitlines()
122
+ assert len(lines) == 3, f"{rel}: a pointer has exactly three lines, got {lines!r}"
123
+ assert lines[0] == POINTER_HEADER
124
+ assert re.fullmatch(r"oid sha256:[0-9a-f]{64}", lines[1]), lines[1]
125
+ assert re.fullmatch(r"size \d+", lines[2]), lines[2]
126
+ assert (root / rel).stat().st_size == len(POINTER) == 132
127
+ assert (root / rel).stat().st_size <= Integrity.THRESHOLD
128
+
129
+
130
+ def expect_pointer_header_over_threshold(root: Path, rel: str) -> None:
131
+ """Precondition: `rel` is over the threshold AND its first line is the pointer header."""
132
+ assert (root / rel).stat().st_size == OVER_THRESHOLD, f"{rel}: expected {OVER_THRESHOLD} bytes"
133
+ assert first_line(root, rel) == POINTER_HEADER
134
+
135
+
136
+ def expect_attributes(root: Path, present: bool) -> None:
137
+ assert (root / ".gitattributes").is_file() is present
138
+
48
139
 
49
- @pytest.fixture(autouse=True)
50
- def _wired(monkeypatch: pytest.MonkeyPatch) -> None:
51
- # LFS-001 is not yet in `checks/__init__.py`'s CHECKS tuple — a later step
52
- # wires this wave of checks into the registry in one shot, to avoid
53
- # concurrent edits to that shared file. Until then, `runner.lookup` cannot
54
- # find this row, so the dispatch path (`runner.run`) is exercised here
55
- # with its one lookup call patched to resolve LFS-001 to this module's
56
- # class — everything else (scope intersection, structural/graded split,
57
- # verdict decision) runs for real.
58
- monkeypatch.setattr(runner, "lookup", lambda gate_id: Integrity)
140
+ def grade(root: Path) -> Verdict:
141
+ files = corpus_files(root)
142
+ verdict = runner.run(ROW, root, files)
143
+ assert verdict.outcome is not Outcome.COULD_NOT_RUN, verdict.render()
144
+ assert verdict.units > 0, f"the runner graded no files: {verdict.render()}"
145
+ assert verdict.units == len(files), f"graded {verdict.units} of {len(files)}: {files}"
146
+ return verdict
59
147
 
60
148
 
61
- def run(case: str) -> object:
62
- root = FIXTURES / case
63
- return runner.run(ROW, root, corpus_files(root))
149
+ # --------------------------------------------------------------- the corpora
150
+
151
+
152
+ def missing_filter(tmp_path: Path) -> Path:
153
+ root = build(tmp_path, {"large file.bin": RAW_OVER})
154
+ expect_attributes(root, present=False)
155
+ expect_raw_blob(root, "large file.bin", OVER_THRESHOLD)
156
+ return root
157
+
158
+
159
+ def attribute_without_conversion(tmp_path: Path) -> Path:
160
+ root = build(tmp_path, {"large file.bin": RAW_OVER}, gitattributes=LFS_ATTRIBUTES)
161
+ expect_attributes(root, present=True)
162
+ assert "filter=lfs" in (root / ".gitattributes").read_text()
163
+ expect_raw_blob(root, "large file.bin", OVER_THRESHOLD)
164
+ return root
165
+
166
+
167
+ def unmatched_extension(tmp_path: Path) -> Path:
168
+ root = build(tmp_path, {"payload.dat": RAW_OVER}, gitattributes=LFS_ATTRIBUTES)
169
+ expect_attributes(root, present=True)
170
+ assert not (root / "payload.dat").name.endswith(".bin")
171
+ expect_raw_blob(root, "payload.dat", OVER_THRESHOLD)
172
+ return root
173
+
174
+
175
+ def clean(tmp_path: Path) -> Path:
176
+ root = build(
177
+ tmp_path,
178
+ {"large file.bin": POINTER, "small.txt": b"small\n"},
179
+ gitattributes=LFS_ATTRIBUTES,
180
+ )
181
+ expect_attributes(root, present=True)
182
+ expect_real_pointer(root, "large file.bin")
183
+ assert (root / "small.txt").stat().st_size <= Integrity.THRESHOLD
184
+ return root
185
+
186
+
187
+ # ------------------------------------------------------------------ the tests
64
188
 
65
189
 
66
190
  def test_the_row_identifies_this_check() -> None:
@@ -68,10 +192,25 @@ def test_the_row_identifies_this_check() -> None:
68
192
  assert ROW.name == "integrity"
69
193
 
70
194
 
71
- def test_a_large_staged_blob_outside_the_lfs_filter_is_rejected() -> None:
195
+ def test_the_real_registry_dispatch_reaches_this_check() -> None:
196
+ # No monkeypatch anywhere in this module: if LFS-001 fell out of
197
+ # `checks/__init__.py`'s CHECKS, every graded case below would come back
198
+ # could-not-run, and this names the cause.
199
+ assert lookup("LFS-001") is Integrity
200
+
201
+
202
+ def test_the_corpus_sizes_sit_either_side_of_the_checks_threshold() -> None:
203
+ # If `THRESHOLD` moves, the boundary cases below stop testing the boundary;
204
+ # this makes that loud rather than silent.
205
+ assert AT_THRESHOLD == Integrity.THRESHOLD
206
+ assert OVER_THRESHOLD == Integrity.THRESHOLD + 1
207
+ assert POINTER_HEADER == Integrity.POINTER
208
+
209
+
210
+ def test_a_large_staged_blob_outside_the_lfs_filter_is_rejected(tmp_path: Path) -> None:
72
211
  # test.sh: `run_gate pre-commit -- "large file.bin"` on a raw 1048577-byte
73
212
  # blob with no `.gitattributes` at all must fail.
74
- verdict = run("missing-filter")
213
+ verdict = grade(missing_filter(tmp_path))
75
214
  assert verdict.outcome is Outcome.FINDINGS, verdict.render()
76
215
  assert len(verdict.findings) == 1
77
216
  finding = verdict.findings[0]
@@ -80,19 +219,19 @@ def test_a_large_staged_blob_outside_the_lfs_filter_is_rejected() -> None:
80
219
  assert finding.line is None
81
220
 
82
221
 
83
- def test_the_filename_with_spaces_is_not_truncated() -> None:
222
+ def test_the_filename_with_spaces_is_not_truncated(tmp_path: Path) -> None:
84
223
  # test.sh: "A space in the name on purpose: a diagnostic that truncates at
85
224
  # the space names a file nobody can find." check.sh's own shell diagnostics
86
225
  # ran the risk of stopping at the first word; `Finding.path` and `render()`
87
226
  # are plain Python strings with no such hazard, asserted directly rather
88
227
  # than by absence.
89
- verdict = run("missing-filter")
228
+ verdict = grade(missing_filter(tmp_path))
90
229
  assert verdict.findings[0].path == "large file.bin"
91
230
  assert "LFS-002 large file.bin: " in verdict.render()
92
231
 
93
232
 
94
- def test_the_finding_names_the_size_and_an_actionable_fix() -> None:
95
- verdict = run("missing-filter")
233
+ def test_the_finding_names_the_size_and_an_actionable_fix(tmp_path: Path) -> None:
234
+ verdict = grade(missing_filter(tmp_path))
96
235
  finding = verdict.findings[0]
97
236
  assert finding.message == (
98
237
  "large file.bin is a 1048577-byte blob entering Git without the LFS filter"
@@ -103,31 +242,605 @@ def test_the_finding_names_the_size_and_an_actionable_fix() -> None:
103
242
  )
104
243
 
105
244
 
106
- def test_a_matching_gitattributes_rule_does_not_excuse_a_raw_blob() -> None:
245
+ def test_a_matching_gitattributes_rule_does_not_excuse_a_raw_blob(tmp_path: Path) -> None:
107
246
  # test.sh: "the .attributes rule is added AFTER the blob was staged...
108
247
  # This is the case that makes the `||` in the check load-bearing." A
109
248
  # `.gitattributes` match alone must not pass a file that was never
110
249
  # actually converted to an LFS pointer.
111
- verdict = run("attribute-without-conversion")
250
+ verdict = grade(attribute_without_conversion(tmp_path))
112
251
  assert verdict.outcome is Outcome.FINDINGS, verdict.render()
113
252
  assert verdict.rules == ["LFS-002"]
114
253
  assert verdict.findings[0].path == "large file.bin"
115
254
 
116
255
 
117
- def test_a_correctly_tracked_lfs_pointer_and_a_small_file_both_stay_silent() -> None:
256
+ def test_a_correctly_tracked_lfs_pointer_and_a_small_file_both_stay_silent(
257
+ tmp_path: Path,
258
+ ) -> None:
118
259
  # test.sh: the renormalized pointer passes at both pre-commit and
119
260
  # qua_deploy, and `small.txt` is silent throughout — three assertions,
120
- # one fixture, per this test module's own docstring.
121
- verdict = run("clean")
261
+ # one corpus, per this test module's own docstring.
262
+ verdict = grade(clean(tmp_path))
122
263
  assert verdict.outcome is Outcome.CLEAN, verdict.render()
123
264
  assert verdict.findings == ()
124
265
 
125
266
 
126
- def test_a_large_ordinary_blob_of_an_unmatched_extension_is_reported() -> None:
267
+ def test_a_large_ordinary_blob_of_an_unmatched_extension_is_reported(tmp_path: Path) -> None:
127
268
  # test.sh: "qua_deploy sees a large ordinary blob in HEAD" — .gitattributes
128
269
  # matches `*.bin` only, so `payload.dat` is committed as an ordinary
129
270
  # object and must still be reported.
130
- verdict = run("unmatched-extension")
271
+ verdict = grade(unmatched_extension(tmp_path))
272
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
273
+ assert verdict.rules == ["LFS-002"]
274
+ assert verdict.findings[0].path == "payload.dat"
275
+
276
+
277
+ # ------------------------------------------------------ the size boundary
278
+
279
+
280
+ def test_a_blob_of_exactly_the_threshold_is_clean(tmp_path: Path) -> None:
281
+ root = build(tmp_path, {"at.bin": RAW_AT})
282
+ expect_attributes(root, present=False)
283
+ expect_raw_blob(root, "at.bin", AT_THRESHOLD)
284
+ verdict = grade(root)
285
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
286
+
287
+
288
+ def test_one_byte_over_the_threshold_is_reported_and_exactly_the_threshold_is_not(
289
+ tmp_path: Path,
290
+ ) -> None:
291
+ # Both in ONE corpus, both graded: the only difference the gate can be
292
+ # reacting to is the single byte.
293
+ root = build(tmp_path, {"at.bin": RAW_AT, "over.bin": RAW_OVER})
294
+ expect_attributes(root, present=False)
295
+ expect_raw_blob(root, "at.bin", AT_THRESHOLD)
296
+ expect_raw_blob(root, "over.bin", OVER_THRESHOLD)
297
+ verdict = grade(root)
298
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
299
+ assert [finding.path for finding in verdict.findings] == ["over.bin"]
300
+ assert verdict.findings[0].message == (
301
+ "over.bin is a 1048577-byte blob entering Git without the LFS filter"
302
+ )
303
+
304
+
305
+ # ------------------------------------------ the pointer branch (over threshold)
306
+
307
+
308
+ def test_a_pointer_header_over_the_threshold_under_a_matching_rule_is_clean(
309
+ tmp_path: Path,
310
+ ) -> None:
311
+ # The only way to reach `check.py`'s pointer comparison: over the size
312
+ # threshold, tracked by a `filter=lfs` rule, and first line == the pointer
313
+ # header. The corpus is `attribute_without_conversion` with one difference,
314
+ # the first line -- and that one line flips the verdict.
315
+ root = build(tmp_path, {"large file.bin": POINTER_HEADER_OVER}, gitattributes=LFS_ATTRIBUTES)
316
+ expect_attributes(root, present=True)
317
+ expect_pointer_header_over_threshold(root, "large file.bin")
318
+ verdict = grade(root)
319
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
320
+ assert verdict.findings == ()
321
+
322
+
323
+ def test_a_pointer_header_over_the_threshold_with_no_tracking_rule_is_reported(
324
+ tmp_path: Path,
325
+ ) -> None:
326
+ # check.py: "a pointer-shaped first line alone does not excuse a path with
327
+ # no rule tracking it" -- both conditions are required, not either one.
328
+ root = build(tmp_path, {"large file.bin": POINTER_HEADER_OVER})
329
+ expect_attributes(root, present=False)
330
+ expect_pointer_header_over_threshold(root, "large file.bin")
331
+ verdict = grade(root)
332
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
333
+ assert verdict.rules == ["LFS-002"]
334
+ assert verdict.findings[0].path == "large file.bin"
335
+
336
+
337
+ def test_a_pointer_header_over_the_threshold_under_a_non_matching_rule_is_reported(
338
+ tmp_path: Path,
339
+ ) -> None:
340
+ root = build(tmp_path, {"payload.dat": POINTER_HEADER_OVER}, gitattributes=LFS_ATTRIBUTES)
341
+ expect_attributes(root, present=True)
342
+ expect_pointer_header_over_threshold(root, "payload.dat")
343
+ verdict = grade(root)
131
344
  assert verdict.outcome is Outcome.FINDINGS, verdict.render()
132
345
  assert verdict.rules == ["LFS-002"]
133
346
  assert verdict.findings[0].path == "payload.dat"
347
+
348
+
349
+ # ------------------------------------- the committed blob, supplied by the CLI
350
+ #
351
+ # `cli.py` is the only module that calls git, so it hands the engine one fact per
352
+ # tracked path: the size of the blob Git STORES for it (`Context.committed_sizes`).
353
+ # A correctly tracked LFS file is a ~132-byte pointer in Git and the full file in
354
+ # the worktree (the filter smudges it on checkout), so the worktree size is the
355
+ # wrong thing to grade whenever a committed size exists. These cases pin what is
356
+ # graded where. A path with NO committed fact is graded by its worktree file, as
357
+ # before -- the existing cases above all run that way, with no facts at all.
358
+
359
+ SMUDGED = 1_170_147 # the measured size of a real smudged LFS file on enact-combined
360
+ POINTER_SIZE = 132 # `git cat-file -s` of the pointer committed for it
361
+
362
+
363
+ def grade_with_facts(root: Path, committed_sizes: Mapping[str, int]) -> Verdict:
364
+ files = corpus_files(root)
365
+ verdict = runner.run(ROW, root, files, committed_sizes=committed_sizes)
366
+ assert verdict.outcome is not Outcome.COULD_NOT_RUN, verdict.render()
367
+ assert verdict.units == len(files), f"graded {verdict.units} of {len(files)}: {files}"
368
+ return verdict
369
+
370
+
371
+ def test_a_smudged_lfs_file_whose_committed_blob_is_a_pointer_is_clean(tmp_path: Path) -> None:
372
+ # The measured false positive: Git stores 132 bytes, the worktree holds the
373
+ # smudged 1,170,147-byte file. Nothing under the repository can fix it -- the
374
+ # path IS correctly tracked -- so the gate must grade what Git stores.
375
+ root = build(tmp_path, {"video.webm": b"\0" * SMUDGED}, gitattributes=LFS_ATTRIBUTES)
376
+ assert (root / "video.webm").stat().st_size == SMUDGED > Integrity.THRESHOLD
377
+ verdict = grade_with_facts(root, {"video.webm": POINTER_SIZE})
378
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
379
+ assert verdict.findings == ()
380
+
381
+
382
+ def test_the_same_smudged_file_is_reported_when_no_committed_size_is_known(
383
+ tmp_path: Path,
384
+ ) -> None:
385
+ # The pair of the case above: the ONLY difference is the committed fact. A
386
+ # missing fact must never silently pass a big file.
387
+ root = build(tmp_path, {"video.webm": b"\0" * SMUDGED}, gitattributes=LFS_ATTRIBUTES)
388
+ verdict = grade_with_facts(root, {})
389
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
390
+ assert [finding.path for finding in verdict.findings] == ["video.webm"]
391
+ assert verdict.findings[0].message == (
392
+ f"video.webm is a {SMUDGED}-byte blob entering Git without the LFS filter"
393
+ )
394
+
395
+
396
+ def test_a_large_committed_blob_is_reported_with_the_committed_size(tmp_path: Path) -> None:
397
+ root = build(tmp_path, {"photo.png": b"\0" * 2_097_152})
398
+ verdict = grade_with_facts(root, {"photo.png": 2_097_152})
399
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
400
+ finding = verdict.findings[0]
401
+ assert finding.rule == "LFS-002"
402
+ assert finding.path == "photo.png"
403
+ assert finding.message == "photo.png is a 2097152-byte blob entering Git without the LFS filter"
404
+ assert finding.fix == (
405
+ "add a .gitattributes rule matching photo.png to filter=lfs, run "
406
+ "'git lfs track' for that pattern, then re-add photo.png"
407
+ )
408
+
409
+
410
+ def test_the_committed_size_decides_not_the_worktree_when_the_two_disagree(
411
+ tmp_path: Path,
412
+ ) -> None:
413
+ # Git stores 2,000,000 bytes; the worktree copy has since been cut down. What
414
+ # Git stores is what enters the repository, so it is still reported -- and the
415
+ # message names the committed size, not the 11 bytes on disk.
416
+ root = build(tmp_path, {"photo.png": b"shrunk\n"})
417
+ verdict = grade_with_facts(root, {"photo.png": 2_000_000})
418
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
419
+ assert verdict.findings[0].message == (
420
+ "photo.png is a 2000000-byte blob entering Git without the LFS filter"
421
+ )
422
+
423
+
424
+ def test_a_committed_blob_exactly_at_the_threshold_is_clean_and_one_over_is_not(
425
+ tmp_path: Path,
426
+ ) -> None:
427
+ # Worktree sizes are deliberately the OPPOSITE of the verdicts, so the only
428
+ # thing that can produce them is the committed size.
429
+ root = build(tmp_path, {"at.bin": RAW_OVER, "over.bin": b"tiny\n"})
430
+ verdict = grade_with_facts(root, {"at.bin": AT_THRESHOLD, "over.bin": OVER_THRESHOLD})
431
+ assert [finding.path for finding in verdict.findings] == ["over.bin"]
432
+ assert verdict.findings[0].message == (
433
+ "over.bin is a 1048577-byte blob entering Git without the LFS filter"
434
+ )
435
+
436
+
437
+ def test_a_path_with_no_committed_size_is_graded_by_its_worktree_beside_one_that_has_one(
438
+ tmp_path: Path,
439
+ ) -> None:
440
+ # `smudged.webm` has a fact (a pointer) and is clean; `new.png` is untracked
441
+ # (no fact) and big, so it is graded exactly as before. One fact existing for
442
+ # another path changes nothing about it.
443
+ root = build(
444
+ tmp_path,
445
+ {"smudged.webm": b"\0" * SMUDGED, "new.png": RAW_OVER},
446
+ gitattributes=LFS_ATTRIBUTES,
447
+ )
448
+ verdict = grade_with_facts(root, {"smudged.webm": POINTER_SIZE})
449
+ assert [finding.path for finding in verdict.findings] == ["new.png"]
450
+ assert verdict.findings[0].message == (
451
+ "new.png is a 1048577-byte blob entering Git without the LFS filter"
452
+ )
453
+
454
+
455
+ def test_a_path_with_a_committed_size_is_graded_even_when_its_worktree_file_is_gone(
456
+ tmp_path: Path,
457
+ ) -> None:
458
+ # In the index but deleted on disk (an unstaged deletion): the blob is still
459
+ # stored, so it is still a large blob outside the LFS filter. Without a fact,
460
+ # an absent worktree file is skipped, as before.
461
+ root = build(tmp_path, {"keep.txt": b"x\n"})
462
+ files = ("keep.txt", "gone.png")
463
+ stored = runner.run(ROW, root, files, committed_sizes={"gone.png": 2_000_000})
464
+ assert [finding.path for finding in stored.findings] == ["gone.png"]
465
+ unknown = runner.run(ROW, root, files, committed_sizes={})
466
+ assert unknown.outcome is Outcome.CLEAN, unknown.render()
467
+
468
+
469
+ # ------------------------------------------------- the shrink-only ratchet register
470
+ #
471
+ # The repository's own register of KNOWN large blobs, `<compliance>/ratchets/
472
+ # LFS-001-large-blobs.yaml`: `over_limit_baseline` maps a repository-relative path
473
+ # to the size in bytes it had when its debt was recorded. It only ever ADDS an
474
+ # allowance (a widening input), so no register means nothing is ratcheted.
475
+ #
476
+ # LFS-002 over the threshold with no entry (as before)
477
+ # LFS-003 over its own recorded entry -- names both sizes
478
+ # LFS-004 an entry that is no longer required, so the register only shrinks
479
+ #
480
+ # `Context` carries no "exhaustive" fact and `core/` is not this gate's to change,
481
+ # so LFS-004 is not gated on the run being whole-tree. It never infers staleness
482
+ # from a path being ABSENT FROM THE SET; it probes each entry's own path exactly
483
+ # the way the set's paths are graded. See check.py's docstring.
484
+
485
+ REGISTER_NAME = "LFS-001-large-blobs.yaml"
486
+ REGISTER_PATH = f"{fixture_trees()['compliance']}/ratchets/{REGISTER_NAME}"
487
+ RECORDED = 2_000_000
488
+ BIG = b"\0" * RECORDED
489
+
490
+
491
+ def register_body(entries: Mapping[str, int] | None = None, *, version: int = 1) -> str:
492
+ lines = [f"schema_version: {version}"]
493
+ if not entries:
494
+ lines.append("over_limit_baseline: {}")
495
+ else:
496
+ lines.append("over_limit_baseline:")
497
+ lines.extend(f' "{path}": {size}' for path, size in entries.items())
498
+ return "\n".join(lines) + "\n"
499
+
500
+
501
+ def write_register(root: Path, body: str) -> None:
502
+ target = root / REGISTER_PATH
503
+ target.parent.mkdir(parents=True, exist_ok=True)
504
+ target.write_text(body)
505
+
506
+
507
+ def ratcheted(
508
+ tmp_path: Path,
509
+ files: Mapping[str, bytes],
510
+ entries: Mapping[str, int],
511
+ gitattributes: str | None = None,
512
+ ) -> Path:
513
+ root = build(tmp_path, {}, gitattributes=gitattributes)
514
+ for rel, content in files.items():
515
+ (root / rel).write_bytes(content)
516
+ write_register(root, register_body(entries))
517
+ return root
518
+
519
+
520
+ def grade_register(
521
+ root: Path,
522
+ *,
523
+ files: Sequence[str] | None = None,
524
+ committed_sizes: Mapping[str, int] | None = None,
525
+ exhaustive: bool = False,
526
+ ) -> Verdict:
527
+ given = tuple(files) if files is not None else corpus_files(root)
528
+ return runner.run(ROW, root, given, exhaustive=exhaustive, committed_sizes=committed_sizes)
529
+
530
+
531
+ def test_a_ratcheted_blob_at_its_recorded_size_is_clean(tmp_path: Path) -> None:
532
+ root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
533
+ assert (root / "photo.png").stat().st_size == RECORDED > Integrity.THRESHOLD
534
+ verdict = grade_register(root)
535
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
536
+ assert verdict.findings == ()
537
+
538
+
539
+ def test_a_ratcheted_blob_that_shrank_but_is_still_over_the_limit_is_clean(tmp_path: Path) -> None:
540
+ smaller = b"\0" * (RECORDED - 1000)
541
+ root = ratcheted(tmp_path, {"photo.png": smaller}, {"photo.png": RECORDED})
542
+ assert Integrity.THRESHOLD < (root / "photo.png").stat().st_size < RECORDED
543
+ verdict = grade_register(root)
544
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
545
+
546
+
547
+ def test_a_ratcheted_blob_that_grew_is_reported_naming_both_sizes(tmp_path: Path) -> None:
548
+ root = ratcheted(tmp_path, {"photo.png": b"\0" * (RECORDED + 1)}, {"photo.png": RECORDED})
549
+ verdict = grade_register(root)
550
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
551
+ assert verdict.rules == ["LFS-003"]
552
+ finding = verdict.findings[0]
553
+ assert finding.path == "photo.png"
554
+ assert str(RECORDED + 1) in finding.message and str(RECORDED) in finding.message
555
+ assert finding.message == (
556
+ f"photo.png is a {RECORDED + 1}-byte blob entering Git without the LFS filter, "
557
+ f"over its ratchet entry of {RECORDED} bytes"
558
+ )
559
+ # The register only shrinks, so the repair never offers to raise the entry.
560
+ assert finding.fix == (
561
+ f"shrink photo.png to at most {RECORDED} bytes, or move it to LFS (add a "
562
+ ".gitattributes rule matching photo.png to filter=lfs, run 'git lfs track' for "
563
+ "that pattern, then re-add photo.png); the register only shrinks, so do not raise "
564
+ "its entry"
565
+ )
566
+
567
+
568
+ def test_a_ratcheted_tracked_blob_is_graded_by_its_committed_size_not_the_worktree(
569
+ tmp_path: Path,
570
+ ) -> None:
571
+ # Worktree is 7 bytes, Git stores RECORDED: at the entry -> clean. One more
572
+ # stored byte -> LFS-003 naming the STORED size, not the 7 bytes on disk.
573
+ root = ratcheted(tmp_path, {"photo.png": b"shrunk\n"}, {"photo.png": RECORDED})
574
+ at = grade_register(root, committed_sizes={"photo.png": RECORDED})
575
+ assert at.outcome is Outcome.CLEAN, at.render()
576
+ over = grade_register(root, committed_sizes={"photo.png": RECORDED + 1})
577
+ assert over.rules == ["LFS-003"], over.render()
578
+ assert str(RECORDED + 1) in over.findings[0].message
579
+
580
+
581
+ def test_an_unratcheted_blob_is_still_reported_beside_a_ratcheted_one(tmp_path: Path) -> None:
582
+ root = ratcheted(tmp_path, {"known.png": BIG, "new.png": BIG}, {"known.png": RECORDED})
583
+ verdict = grade_register(root)
584
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
585
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "new.png")]
586
+ assert verdict.findings[0].message == (
587
+ f"new.png is a {RECORDED}-byte blob entering Git without the LFS filter"
588
+ )
589
+
590
+
591
+ def test_an_entry_does_not_excuse_a_different_path(tmp_path: Path) -> None:
592
+ root = ratcheted(tmp_path, {"other.png": BIG}, {"known.png": RECORDED})
593
+ verdict = grade_register(root)
594
+ assert ("LFS-002", "other.png") in [(f.rule, f.path) for f in verdict.findings]
595
+
596
+
597
+ # ------------------------------------------------------ stale entries (LFS-004)
598
+
599
+
600
+ def test_an_entry_for_a_file_now_at_the_threshold_is_stale(tmp_path: Path) -> None:
601
+ root = ratcheted(tmp_path, {"photo.png": RAW_AT}, {"photo.png": RECORDED})
602
+ verdict = grade_register(root)
603
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
604
+ assert verdict.rules == ["LFS-004"]
605
+ finding = verdict.findings[0]
606
+ assert finding.path == "photo.png"
607
+ assert finding.message == (
608
+ "ratchet entry for photo.png is no longer required: it is 1048576 bytes, "
609
+ "at or under the 1048576-byte threshold"
610
+ )
611
+ assert finding.fix == f"delete photo.png from over_limit_baseline in {REGISTER_PATH}"
612
+
613
+
614
+ def test_an_entry_for_a_file_that_is_gone_is_stale(tmp_path: Path) -> None:
615
+ root = ratcheted(tmp_path, {"keep.txt": b"x\n"}, {"gone.png": RECORDED})
616
+ verdict = grade_register(root)
617
+ assert verdict.rules == ["LFS-004"], verdict.render()
618
+ assert verdict.findings[0].path == "gone.png"
619
+ assert verdict.findings[0].message == (
620
+ "ratchet entry for gone.png is no longer required: it is not a file in the tree"
621
+ )
622
+
623
+
624
+ def test_an_entry_for_a_file_git_now_stores_as_a_pointer_is_stale(tmp_path: Path) -> None:
625
+ # Moved to LFS: Git stores the ~132-byte pointer while the worktree holds the
626
+ # smudged full file, so the worktree size would wrongly keep the entry alive.
627
+ root = ratcheted(
628
+ tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED}, gitattributes="*.png filter=lfs\n"
629
+ )
630
+ verdict = grade_register(root, committed_sizes={"photo.png": POINTER_SIZE})
631
+ assert verdict.rules == ["LFS-004"], verdict.render()
632
+ assert verdict.findings[0].message == (
633
+ "ratchet entry for photo.png is no longer required: Git stores 132 bytes for it, "
634
+ "at or under the 1048576-byte threshold"
635
+ )
636
+
637
+
638
+ def test_an_entry_for_a_pointer_shaped_file_under_a_matching_rule_is_stale(
639
+ tmp_path: Path,
640
+ ) -> None:
641
+ root = ratcheted(
642
+ tmp_path,
643
+ {"large file.bin": POINTER_HEADER_OVER},
644
+ {"large file.bin": OVER_THRESHOLD},
645
+ gitattributes=LFS_ATTRIBUTES,
646
+ )
647
+ expect_pointer_header_over_threshold(root, "large file.bin")
648
+ verdict = grade_register(root)
649
+ assert verdict.rules == ["LFS-004"], verdict.render()
650
+ assert verdict.findings[0].message == (
651
+ "ratchet entry for large file.bin is no longer required: "
652
+ "it is an LFS pointer under a matching .gitattributes rule"
653
+ )
654
+
655
+
656
+ def test_a_ratcheted_blob_and_its_stale_neighbour_are_told_apart(tmp_path: Path) -> None:
657
+ root = ratcheted(
658
+ tmp_path,
659
+ {"live.png": BIG, "small.png": b"tiny\n"},
660
+ {"live.png": RECORDED, "small.png": RECORDED, "gone.png": RECORDED},
661
+ )
662
+ verdict = grade_register(root)
663
+ assert sorted((f.rule, f.path) for f in verdict.findings) == [
664
+ ("LFS-004", "gone.png"),
665
+ ("LFS-004", "small.png"),
666
+ ]
667
+
668
+
669
+ # ---------------- staleness is probed per entry, never inferred from the given set
670
+
671
+
672
+ def test_an_entry_whose_file_the_set_does_not_name_but_is_still_big_is_not_stale(
673
+ tmp_path: Path,
674
+ ) -> None:
675
+ # The false positive a narrower set must not create: `live.png` is simply not
676
+ # in this partial set, and a gate that read "unseen" as "stale" would report it.
677
+ root = ratcheted(tmp_path, {"live.png": BIG, "other.txt": b"x\n"}, {"live.png": RECORDED})
678
+ for exhaustive in (False, True):
679
+ verdict = grade_register(root, files=("other.txt",), exhaustive=exhaustive)
680
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
681
+
682
+
683
+ def test_a_stale_entry_is_found_by_probing_its_path_whether_or_not_the_set_names_it(
684
+ tmp_path: Path,
685
+ ) -> None:
686
+ # The pair of the case above: `small.png` is no longer required whether the
687
+ # set lists it or not, and `exhaustive` changes neither answer. `Context`
688
+ # does not carry that fact, so this pins the behaviour that stands in for it.
689
+ root = ratcheted(
690
+ tmp_path, {"small.png": b"tiny\n", "other.txt": b"x\n"}, {"small.png": RECORDED}
691
+ )
692
+ for given in (("other.txt",), ("other.txt", "small.png")):
693
+ for exhaustive in (False, True):
694
+ verdict = grade_register(root, files=given, exhaustive=exhaustive)
695
+ assert verdict.rules == ["LFS-004"], (given, exhaustive, verdict.render())
696
+
697
+
698
+ # --------------------------------------------- a register that is not usable
699
+
700
+
701
+ FIXTURES = Path(__file__).parent / "fixtures"
702
+ MALFORMED = sorted((FIXTURES / "malformed").glob("*.yaml"))
703
+
704
+
705
+ def test_the_malformed_fixtures_are_all_there() -> None:
706
+ # An empty glob would parametrize to zero cases and pass having tested nothing.
707
+ assert len(MALFORMED) == 22, [path.name for path in MALFORMED]
708
+
709
+
710
+ @pytest.mark.parametrize("fixture", MALFORMED, ids=[path.stem for path in MALFORMED])
711
+ def test_a_register_that_exists_but_is_malformed_is_could_not_run(
712
+ tmp_path: Path, fixture: Path
713
+ ) -> None:
714
+ # Loud and exit 2, never ignored: ignoring it would grade every ratcheted
715
+ # blob as a fresh violation, or worse, silently drop an entry's allowance.
716
+ root = build(tmp_path, {"photo.png": BIG})
717
+ write_register(root, fixture.read_text(encoding="utf-8"))
718
+ verdict = grade_register(root)
719
+ assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
720
+ assert REGISTER_PATH in verdict.message
721
+ assert "ratchet register" in verdict.message
722
+
723
+
724
+ def test_the_committed_valid_register_ratchets_exactly_what_it_names(tmp_path: Path) -> None:
725
+ # `fixtures/valid.yaml` is a real register: a plain path and a nested path with
726
+ # a space in it. Files at their recorded sizes are clean; the one that grew is
727
+ # not, and the entry for the path nobody created is stale.
728
+ root = build(tmp_path, {"photo.png": b"\0" * 2_000_000})
729
+ (root / "assets").mkdir()
730
+ (root / "assets" / "large file.bin").write_bytes(b"\0" * 1_500_001)
731
+ write_register(root, (FIXTURES / "valid.yaml").read_text(encoding="utf-8"))
732
+ verdict = grade_register(root)
733
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-003", "assets/large file.bin")]
734
+ (root / "assets" / "large file.bin").write_bytes(b"\0" * 1_500_000)
735
+ clean = grade_register(root)
736
+ assert clean.outcome is Outcome.CLEAN, clean.render()
737
+ (root / "photo.png").unlink()
738
+ gone = grade_register(root)
739
+ assert [(f.rule, f.path) for f in gone.findings] == [("LFS-004", "photo.png")]
740
+
741
+
742
+ def test_a_register_path_that_is_a_directory_is_could_not_run(tmp_path: Path) -> None:
743
+ root = build(tmp_path, {"photo.png": BIG})
744
+ (root / REGISTER_PATH).mkdir(parents=True)
745
+ verdict = grade_register(root)
746
+ assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
747
+ assert "ratchet register" in verdict.message
748
+
749
+
750
+ def test_a_duplicate_path_is_refused_even_when_the_last_value_would_have_passed(
751
+ tmp_path: Path,
752
+ ) -> None:
753
+ # PyYAML keeps the LAST of a repeated key. "Lowering" an entry by adding a
754
+ # second line while leaving the old, higher one in place would then be ignored
755
+ # without a word, so a repeated path is a malformed register.
756
+ root = build(tmp_path, {"photo.png": BIG})
757
+ write_register(
758
+ root,
759
+ f'schema_version: 1\nover_limit_baseline:\n "photo.png": {RECORDED - 1}\n'
760
+ f' "photo.png": {RECORDED}\n',
761
+ )
762
+ verdict = grade_register(root)
763
+ assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
764
+ assert "photo.png" in verdict.message
765
+
766
+
767
+ # ------------------------------------------------ no register: grading as before
768
+
769
+
770
+ def test_an_absent_register_grades_exactly_as_before(tmp_path: Path) -> None:
771
+ root = build(tmp_path, {"photo.png": BIG})
772
+ assert not (root / REGISTER_PATH).exists()
773
+ verdict = grade_register(root)
774
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
775
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
776
+ assert verdict.findings[0].message == (
777
+ f"photo.png is a {RECORDED}-byte blob entering Git without the LFS filter"
778
+ )
779
+
780
+
781
+ def test_an_empty_baseline_grades_exactly_as_before(tmp_path: Path) -> None:
782
+ root = ratcheted(tmp_path, {"photo.png": BIG}, {})
783
+ verdict = grade_register(root)
784
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
785
+
786
+
787
+ def test_a_register_whose_tree_the_repository_did_not_declare_is_dropped_not_fatal(
788
+ tmp_path: Path,
789
+ ) -> None:
790
+ # `resolve_inputs`' widening branch (runner.py): the effective map knows the
791
+ # tree but the repository's OWN declarations do not. The register is never
792
+ # read, a widening input grades STRICTER when absent, and the file its entry
793
+ # would have excused is reported as before.
794
+ root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
795
+ verdict = runner.run(ROW, root, corpus_files(root), declared_trees={})
796
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
797
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
798
+
799
+
800
+ def test_a_compliance_tree_declared_absent_drops_the_register(tmp_path: Path) -> None:
801
+ # `compliance = ""` is the documented way to say "this repository has no such
802
+ # tree" (`policy.config.absent`): the input is skipped, so a repository with no
803
+ # compliance tree keeps grading LFS-001 exactly as before.
804
+ root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
805
+ trees = {**fixture_trees(), "compliance": ""}
806
+ verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
807
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
808
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
809
+
810
+
811
+ def test_a_compliance_tree_missing_from_the_map_is_could_not_run_naming_what_to_declare(
812
+ tmp_path: Path,
813
+ ) -> None:
814
+ # PINS A GAP, not a goal. `resolve_inputs` expands every declared input before
815
+ # it reaches the widening branch, so a tree ABSENT from the map (neither a path
816
+ # nor "") is could-not-run even for a widening input -- reproduced through the
817
+ # real CLI on enact-combined with no `--tree compliance=...`. The message names
818
+ # exactly what to declare, and both repositories in this estate declare it. The
819
+ # fix is in `runner.resolve_inputs`, not in this check; this test flips with it.
820
+ root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
821
+ trees = {name: path for name, path in fixture_trees().items() if name != "compliance"}
822
+ verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
823
+ assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
824
+ assert "[controls.compliance.trees].compliance" in verdict.message
825
+
826
+
827
+ # --------------------------------------------------------- the register's shape
828
+
829
+
830
+ def test_the_register_is_a_widening_consumer_input() -> None:
831
+ assert Integrity.inputs == {"register": f"<compliance>/ratchets/{REGISTER_NAME}"}
832
+ assert "register" in Integrity.widening_inputs
833
+ assert "register" not in Integrity.resources
834
+
835
+
836
+ def test_editing_the_register_selects_the_gate() -> None:
837
+ # No `<compliance>` scope entry is needed (and one would make the row
838
+ # could-not-run in a repository that declares no such tree): `**` already
839
+ # selects the register, so a commit that edits it is graded.
840
+ assert scope.intersect(ROW.engine.scope, [REGISTER_PATH]) == (REGISTER_PATH,)
841
+
842
+
843
+ def test_the_new_finding_ids_are_distinct_and_collide_with_no_row() -> None:
844
+ ids = {Integrity.rule, Integrity.GREW, Integrity.STALE}
845
+ assert ids == {"LFS-002", "LFS-003", "LFS-004"}
846
+ assert not ids & {row.id for row in load().rows}