@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.10 → 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 (413) hide show
  1. package/bin/adl +2 -2
  2. package/bin/git +17 -18
  3. package/bin/git-ado +27 -26
  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/bun-compile-build.sh +1 -1
  9. package/bin/lib/fleet-host.sh +62 -0
  10. package/bin/lib/lean-ctx-observe-hooks.sh +9 -9
  11. package/bin/reconcile +693 -470
  12. package/bin/reconcile-host +121 -0
  13. package/bin/setup +219 -0
  14. package/bin/shell-path-policy +15 -5
  15. package/bin/sync-and-reconcile +79 -68
  16. package/bin/validate-manifest +46 -51
  17. package/package.json +2 -12
  18. package/packages/agent-hooks/README.md +14 -9
  19. package/packages/agent-hooks/assets/hooks.json +0 -20
  20. package/packages/agent-hooks/package.json +1 -2
  21. package/packages/agent-hooks/src/cli/args.ts +1 -1
  22. package/packages/agent-hooks/src/cli/doctor.ts +6 -58
  23. package/packages/agent-hooks/src/deploy/deploy.ts +12 -41
  24. package/packages/agent-hooks/src/deploy/doctor.ts +8 -11
  25. package/packages/agent-hooks/src/deploy/index.ts +14 -14
  26. package/packages/agent-hooks/src/deploy/json-merge.ts +7 -8
  27. package/packages/agent-hooks/src/deploy/legacy-lock.ts +2 -2
  28. package/packages/agent-hooks/src/deploy/list.ts +1 -1
  29. package/packages/agent-hooks/src/deploy/lock.ts +31 -12
  30. package/packages/agent-hooks/src/deploy/profile.ts +3 -4
  31. package/packages/agent-hooks/src/deploy/source.ts +1 -1
  32. package/packages/agent-hooks/src/gating/config.ts +10 -24
  33. package/packages/agent-hooks/src/gating/root.ts +3 -3
  34. package/packages/agent-hooks/src/hooks/briefing/handler.ts +2 -2
  35. package/packages/agent-hooks/src/hooks/briefing/index.ts +27 -287
  36. package/packages/agent-hooks/src/hooks/contract.ts +17 -17
  37. package/packages/agent-hooks/src/hooks/crg/background.ts +27 -0
  38. package/packages/agent-hooks/src/hooks/crg/index.ts +7 -6
  39. package/packages/agent-hooks/src/hooks/log.ts +8 -10
  40. package/packages/agent-hooks/src/hooks/user-prompt-submit/match.ts +1 -1
  41. package/packages/agent-hooks/src/hooks/user-prompt-submit/tokenizer.ts +1 -1
  42. package/packages/agent-hooks/src/hooks/user-prompt-submit/vector.ts +6 -5
  43. package/packages/agent-hooks/src/hosts/registry.ts +1 -1
  44. package/packages/agent-hooks/src/log/adl-dir.ts +44 -0
  45. package/packages/agent-hooks/src/log/writer.ts +16 -2
  46. package/packages/agent-hooks/src/main.ts +7 -41
  47. package/packages/agent-hooks/src/pre-tool-use/handler.ts +71 -82
  48. package/packages/agent-hooks/src/rules/engine.ts +13 -17
  49. package/packages/agent-hooks/src/rules/impl/adl-build-run-denied.ts +3 -3
  50. package/packages/agent-hooks/src/rules/impl/adl-git-credential-bypass.ts +12 -14
  51. package/packages/agent-hooks/src/rules/impl/adl-git-identity.ts +28 -28
  52. package/packages/agent-hooks/src/rules/impl/adl-git-push-policy.ts +8 -9
  53. package/packages/agent-hooks/src/rules/impl/adl-human-azdo-bypass.ts +56 -59
  54. package/packages/agent-hooks/src/rules/impl/adl-identity.ts +16 -23
  55. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +19 -16
  56. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +2 -2
  57. package/packages/agent-hooks/src/rules/impl/adl-pr-review-role.ts +9 -12
  58. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +5 -7
  59. package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +411 -0
  60. package/packages/agent-hooks/src/rules/impl/adl-token-minting-role.ts +4 -5
  61. package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +6 -7
  62. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +77 -70
  63. package/packages/agent-hooks/src/rules/impl/deny-allowlist.ts +159 -0
  64. package/packages/agent-hooks/src/rules/impl/fs-trash.ts +2 -2
  65. package/packages/agent-hooks/src/rules/impl/git-helpers.ts +21 -23
  66. package/packages/agent-hooks/src/rules/impl/git-worktree.ts +14 -28
  67. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +3 -3
  68. package/packages/agent-hooks/src/rules/impl/messages.ts +1 -1
  69. package/packages/agent-hooks/src/rules/impl/pr-completion.ts +1 -1
  70. package/packages/agent-hooks/src/rules/impl/tool-azure-devops.ts +35 -46
  71. package/packages/agent-hooks/src/rules/impl/tool-find.ts +34 -7
  72. package/packages/agent-hooks/src/rules/impl/tool-grep.ts +34 -12
  73. package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +52 -9
  74. package/packages/agent-hooks/src/rules/ports.ts +5 -7
  75. package/packages/agent-hooks/src/rules/registry.ts +4 -6
  76. package/packages/agent-hooks/src/rules/shell/git-identity.ts +86 -101
  77. package/packages/agent-hooks/src/rules/shell/git-internals.ts +1 -1
  78. package/packages/agent-hooks/src/rules/shell/invocation.ts +24 -118
  79. package/packages/agent-hooks/src/rules/shell/piped-script.ts +1 -1
  80. package/packages/agent-hooks/src/rules/shell/prefixes.ts +1 -1
  81. package/packages/agent-hooks/src/rules/shell/protected-carrier-writes.ts +11 -10
  82. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +26 -26
  83. package/packages/agent-hooks/src/rules/shell/substitution.ts +1 -1
  84. package/packages/agent-hooks/src/rules/shell/xargs.ts +2 -2
  85. package/packages/agent-hooks/src/rules/spec.ts +2 -2
  86. package/packages/agent-settings/README.md +287 -0
  87. package/packages/agent-settings/assets/GLOBAL-AGENTS.md +18 -18
  88. package/packages/agent-settings/assets/agents/architect.toml +2 -0
  89. package/packages/agent-settings/assets/agents/code-reviewer.toml +2 -0
  90. package/packages/agent-settings/assets/agents/executor.toml +2 -0
  91. package/packages/agent-settings/assets/agents/explore.toml +2 -0
  92. package/packages/agent-settings/assets/agents/planner.toml +2 -0
  93. package/packages/agent-settings/assets/mcp.json +0 -4
  94. package/packages/agent-settings/deploy-profile.json +139 -0
  95. package/packages/agent-settings/deploy-profile.schema.json +147 -0
  96. package/packages/agent-settings/managed/README.md +29 -30
  97. package/packages/agent-settings/managed/codex-requirements.toml +9 -9
  98. package/packages/agent-settings/package.json +20 -0
  99. package/packages/agent-settings/src/deploy/agent-render.ts +2 -2
  100. package/packages/agent-settings/src/deploy/agent-toml.ts +1 -1
  101. package/packages/agent-settings/src/deploy/agents-deploy.ts +17 -3
  102. package/packages/agent-settings/src/deploy/doctor.ts +3 -83
  103. package/packages/agent-settings/src/deploy/index.ts +6 -33
  104. package/packages/agent-settings/src/deploy/legacy-lock.ts +6 -6
  105. package/packages/agent-settings/src/deploy/list.ts +5 -19
  106. package/packages/agent-settings/src/deploy/lock.ts +8 -18
  107. package/packages/agent-settings/src/deploy/mcp-deploy.ts +10 -5
  108. package/packages/agent-settings/src/deploy/mcp-merge.ts +22 -3
  109. package/packages/agent-settings/src/deploy/mcp-source.ts +1 -1
  110. package/packages/agent-settings/src/deploy/profile.ts +11 -35
  111. package/packages/agent-settings/src/deploy/retire-lean-ctx.ts +184 -0
  112. package/packages/agent-settings/src/deploy/settings.ts +17 -22
  113. package/packages/agent-settings/src/global-surface-sweep.ts +5 -17
  114. package/packages/agent-settings/src/kimi-hooks.ts +339 -192
  115. package/packages/agent-settings/src/opencode-hooks.ts +1 -1
  116. package/packages/agent-settings/tsconfig.json +23 -0
  117. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +120 -0
  118. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +276 -0
  119. package/packages/agent-skills/assets/debug-issue/SKILL.md +8 -7
  120. package/packages/agent-skills/assets/explore-codebase/SKILL.md +13 -13
  121. package/packages/agent-skills/assets/refactor-safely/SKILL.md +12 -11
  122. package/packages/agent-skills/assets/review-changes/SKILL.md +7 -7
  123. package/packages/agent-skills/assets/testing/references/e2e.md +37 -0
  124. package/packages/agent-skills/assets/testing/references/integration.md +37 -0
  125. package/packages/agent-skills/assets/testing/references/unit.md +32 -0
  126. package/packages/agent-skills/dist/deploy.d.ts +2 -0
  127. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  128. package/packages/agent-skills/dist/deploy.js +27 -1
  129. package/packages/agent-skills/dist/deploy.js.map +1 -1
  130. package/packages/agent-skills/dist/doctor.d.ts +0 -2
  131. package/packages/agent-skills/dist/doctor.d.ts.map +1 -1
  132. package/packages/agent-skills/dist/doctor.js +1 -1
  133. package/packages/agent-skills/dist/doctor.js.map +1 -1
  134. package/packages/agent-skills/dist/fs-utils.d.ts +0 -1
  135. package/packages/agent-skills/dist/fs-utils.d.ts.map +1 -1
  136. package/packages/agent-skills/dist/fs-utils.js +1 -1
  137. package/packages/agent-skills/dist/fs-utils.js.map +1 -1
  138. package/packages/agent-skills/package.json +1 -1
  139. package/packages/agent-skills/src/deploy.ts +29 -1
  140. package/packages/agent-skills/src/doctor.ts +1 -1
  141. package/packages/agent-skills/src/fs-utils.ts +1 -1
  142. package/packages/azdo/README.md +10 -9
  143. package/packages/azdo/package.json +2 -5
  144. package/packages/azdo/src/azdo-pl.ts +2 -54
  145. package/packages/azdo/src/azdo-pr-review.ts +4 -4
  146. package/packages/azdo/src/azdo-repo-doctor.ts +101 -52
  147. package/packages/azdo/src/azdo-wit.ts +41 -2
  148. package/packages/azdo/src/lib/agent-identity.ts +12 -14
  149. package/packages/azdo/src/lib/agent-token.ts +2 -2
  150. package/packages/azdo/src/lib/client.ts +0 -5
  151. package/packages/azdo/src/lib/graph-identity.ts +4 -4
  152. package/packages/azdo/src/workflow-rules-matrix.csv +85 -0
  153. package/packages/azdo/src/workflow-rules.ts +128 -0
  154. package/packages/fleet/manifest.json +38 -33
  155. package/packages/fleet/manifest.schema.json +67 -65
  156. package/packages/fleet/src/default-agent-file.ts +3 -3
  157. package/packages/fleet/src/install-adl-package.ts +73 -15
  158. package/packages/fleet/src/install-embedding-model.ts +14 -14
  159. package/packages/fleet/src/managed-settings.ts +7 -7
  160. package/packages/fleet/src/repo-maintain.ts +439 -0
  161. package/packages/fleet/src/scratch-reap.ts +6 -6
  162. package/packages/fleet/src/vscode-settings.ts +5 -5
  163. package/packages/repo-controls/README.md +141 -43
  164. package/packages/repo-controls/assets/promote.ts +4 -4
  165. package/packages/repo-controls/assets/reconcile-guard.sh +10 -46
  166. package/packages/repo-controls/assets/version-bump.sh +31 -84
  167. package/packages/repo-controls/bunfig.toml +6 -7
  168. package/packages/repo-controls/package/adl-compliance/AGENTS.md +5 -6
  169. package/packages/repo-controls/package/adl-compliance/conftest.py +3 -1
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +4 -16
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +9 -2
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/notes.md +1 -0
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +11 -0
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/__init__.py +7 -0
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/check.py +266 -0
  176. 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
  177. 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
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.hidden.md +1 -0
  179. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/a.md +1 -0
  180. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/b.md +1 -0
  181. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/data.csv +1 -0
  182. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/diagram.png +1 -0
  183. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/mock.png +1 -0
  184. 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
  185. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs.md +1 -0
  186. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/grants.yaml +4 -0
  187. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p1.md +1 -0
  188. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p2.md +1 -0
  189. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p3.md +1 -0
  190. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p4.md +1 -0
  191. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p5.md +1 -0
  192. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/build.md +1 -0
  193. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/overview.md +1 -0
  194. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/install.md +1 -0
  195. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/start.md +1 -0
  196. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/grants.yaml +4 -0
  197. 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
  198. 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
  199. 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
  200. 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
  201. 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
  202. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/a.md +1 -0
  203. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/b.md +1 -0
  204. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/c.md +1 -0
  205. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/grants.yaml +4 -0
  206. 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
  207. 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
  208. 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
  209. 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
  210. 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
  211. 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
  212. 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
  213. 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
  214. 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
  215. 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
  216. 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
  217. 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
  218. 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
  219. 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
  220. 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
  221. 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
  222. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/grants.yaml +4 -0
  223. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/a.md +1 -0
  224. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/b.md +1 -0
  225. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/c.md +1 -0
  226. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/grants.yaml +4 -0
  227. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/a.md +1 -0
  228. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/b.md +1 -0
  229. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/c.md +1 -0
  230. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/grants.yaml +4 -0
  231. 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
  232. 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
  233. 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
  234. 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
  235. 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
  236. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/grants.yaml +4 -0
  237. 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
  238. 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
  239. 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
  240. 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
  241. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/grants.yaml +4 -0
  242. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/a.md +1 -0
  243. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/b.md +1 -0
  244. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/c.md +1 -0
  245. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/d.md +1 -0
  246. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/a.md +1 -0
  247. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/b.md +1 -0
  248. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/grants.yaml +6 -0
  249. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/a.md +1 -0
  250. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/b.md +1 -0
  251. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/c.md +1 -0
  252. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/grants.yaml +3 -0
  253. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/test_check.py +345 -0
  254. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +18 -5
  255. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +35 -0
  256. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/__init__.py +7 -0
  257. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/check.py +91 -0
  258. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/AGENTS.md +3 -0
  259. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/CLAUDE.md +3 -0
  260. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/-leading-hyphen.md +3 -0
  261. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/AGENTS.md +3 -0
  262. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/double--hyphen.md +3 -0
  263. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/getting started.md +3 -0
  264. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/nested/page-here.md +3 -0
  265. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/trailing-hyphen-.md +3 -0
  266. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +4 -0
  267. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/user_profile.md +3 -0
  268. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/UI-CONTRACT.png +2 -0
  269. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/diagram.png +1 -0
  270. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/index.md +4 -0
  271. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/notes.md +3 -0
  272. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/reference.html +1 -0
  273. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/2026-10-experiment.md +3 -0
  274. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +6 -0
  275. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs.md +5 -0
  276. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +194 -0
  277. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +14 -2
  278. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +5 -5
  279. 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
  280. 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
  281. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +58 -6
  282. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +0 -1
  283. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/check.py +64 -17
  284. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +60 -9
  285. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-clean/docs/notes.md +17 -0
  286. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-no-lines/docs/notes.md +13 -0
  287. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs/architecture/demo.md +0 -1
  288. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs-okf-profiles.yaml +0 -1
  289. 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
  290. 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
  291. 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
  292. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs-okf-profiles.yaml +5 -0
  293. 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
  294. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs-okf-profiles.yaml +0 -1
  295. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/experiment-ok.md +15 -0
  296. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +15 -0
  297. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +16 -0
  298. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +11 -0
  299. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +11 -0
  300. 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
  301. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-sources.md +10 -0
  302. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +11 -0
  303. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +11 -0
  304. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +16 -0
  305. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +14 -0
  306. 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
  307. 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
  308. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/operations/runbook.md +26 -0
  309. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +228 -2
  310. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +5 -0
  311. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +11 -2
  312. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/__init__.py +5 -3
  313. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +160 -44
  314. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +0 -2
  315. 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
  316. 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
  317. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/grants.yaml +3 -0
  318. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +0 -2
  319. 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
  320. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +5 -0
  321. 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
  322. 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
  323. 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
  324. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +4 -0
  325. 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
  326. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +5 -0
  327. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +0 -2
  328. 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
  329. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/grants.yaml +3 -0
  330. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/register.py +127 -0
  331. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +265 -29
  332. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +75 -2
  333. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +194 -0
  334. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +56 -10
  335. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +256 -0
  336. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +40 -12
  337. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +108 -0
  338. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +1 -1
  339. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +2 -3
  340. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +23 -6
  341. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +76 -0
  342. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +58 -17
  343. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +161 -0
  344. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +35 -3
  345. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +108 -1
  346. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +281 -61
  347. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/absolute-path.yaml +3 -0
  348. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/backslash-path.yaml +3 -0
  349. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/baseline-is-a-list.yaml +2 -0
  350. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/boolean-size.yaml +3 -0
  351. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/dot-segment.yaml +3 -0
  352. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/duplicate-path.yaml +4 -0
  353. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-file.yaml +0 -0
  354. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-segment.yaml +3 -0
  355. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/float-size.yaml +3 -0
  356. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/negative-size.yaml +3 -0
  357. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-over-limit-baseline.yaml +1 -0
  358. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-schema-version.yaml +1 -0
  359. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/non-string-path.yaml +3 -0
  360. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/not-yaml.yaml +2 -0
  361. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/null-size.yaml +3 -0
  362. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/parent-segment.yaml +3 -0
  363. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/string-size.yaml +3 -0
  364. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/top-level-is-a-list.yaml +2 -0
  365. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/trailing-slash.yaml +3 -0
  366. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/unknown-key.yaml +3 -0
  367. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/wrong-schema-version.yaml +2 -0
  368. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/zero-size.yaml +3 -0
  369. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/valid.yaml +4 -0
  370. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +758 -45
  371. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/check.py +4 -3
  372. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/test_check.py +2 -2
  373. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +490 -0
  374. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +368 -55
  375. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +4 -2
  376. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +523 -0
  377. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +1179 -0
  378. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +122 -3
  379. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +33 -1
  380. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +0 -11
  381. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/context.py +16 -1
  382. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -2
  383. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +12 -1
  384. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +133 -147
  385. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +133 -147
  386. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  387. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +52 -10
  388. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +23 -4
  389. package/packages/repo-controls/package.json +2 -10
  390. package/packages/repo-controls/scripts/hooks/post-merge +13 -0
  391. package/packages/repo-controls/scripts/hooks/pre-push +3 -3
  392. package/packages/repo-controls/src/azdo-push.ts +64 -36
  393. package/packages/repo-controls/src/azure-devops-governance.ts +7 -9
  394. package/packages/repo-controls/src/azure-devops-remote.ts +1 -1
  395. package/packages/repo-controls/src/cli.ts +29 -8
  396. package/packages/repo-controls/src/compliance-config.ts +410 -2
  397. package/packages/repo-controls/src/config.ts +133 -60
  398. package/packages/repo-controls/src/constants.ts +13 -27
  399. package/packages/repo-controls/src/coverage-ratchet.ts +4 -4
  400. package/packages/repo-controls/src/doctor.ts +46 -19
  401. package/packages/repo-controls/src/gitignore-block.ts +5 -29
  402. package/packages/repo-controls/src/hook-log.ts +1 -2
  403. package/packages/repo-controls/src/hook.ts +201 -45
  404. package/packages/repo-controls/src/install.ts +14 -8
  405. package/packages/repo-controls/src/integrity.ts +3 -3
  406. package/packages/repo-controls/src/lfs.ts +111 -0
  407. package/packages/repo-controls/src/promote.ts +8 -8
  408. package/packages/repo-controls/src/runtime.ts +9 -1
  409. package/packages/repo-controls/src/workflows-drift.ts +4 -5
  410. package/packages/repo-controls/src/worktree-guard.ts +3 -3
  411. package/packages/fleet/jobs/git-maintain/src/main.ts +0 -848
  412. package/packages/fleet/systemd/adl-azure-agent-update.service +0 -13
  413. package/packages/fleet/systemd/adl-azure-agent-update.timer +0 -10
@@ -0,0 +1,1179 @@
1
+ """REP-002 `LCK-002` grades the CONTENT of a `Cargo.lock`, not only cargo's exit code.
2
+
3
+ The hole this pins (measured through the real check on 2026-10-01, probe
4
+ `.scratchpad/rep002-scope/probe_stale.py`): the gate ran `cargo metadata --locked
5
+ --offline --no-deps` and reported `LCK-002` only when it exited non-zero. With
6
+ `--no-deps` cargo never resolves, so it never reads `Cargo.lock`: a lock that was
7
+ `version = 4` alone, one that was not TOML, and one with `version = 99` all came
8
+ out CLEAN, and only a missing or empty lock was ever reported.
9
+
10
+ The validation under test is deterministic Python (`tomllib`): it reads the lock,
11
+ the workspace manifest, the member manifests and the path-dependency manifests the
12
+ manifest text names, and nothing else. No cargo, no registry, no network.
13
+
14
+ Two kinds of test, and why:
15
+
16
+ * Most tests grade through the real dispatch path (`runner.run`) with the two
17
+ cargo subprocess calls replaced by a recorder that exits 0 (`cargo_accepts`),
18
+ the same technique `test_declared_paths.py` uses for the cargo cwd pin. The
19
+ content validation never asks cargo anything, so these tests need no toolchain
20
+ and cannot depend on a machine's registry cache; the recorder also pins that
21
+ cargo is asked exactly the two questions it always was.
22
+ * A few tests (`requires_cargo`) run the REAL `cargo metadata --no-deps` against
23
+ hand-written manifests, so the integration with the real command is proven too.
24
+
25
+ Fixtures are hand-written text, never generated by cargo: a valid lock is TOML
26
+ (`version = 3`, `[[package]]` with `name`/`version`/`source`/`checksum`), and a
27
+ path package (a workspace member, a path dependency) has no `source`.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import shutil
33
+ from collections.abc import Mapping
34
+ from pathlib import Path
35
+ from types import SimpleNamespace
36
+ from typing import NamedTuple
37
+
38
+ import pytest
39
+ from fixture_trees import fixture_trees
40
+ from helpers import corpus_files
41
+
42
+ from adl_compliance import runner
43
+ from adl_compliance.checks.repository.lockfile_integrity import check as module
44
+ from adl_compliance.core.verdict import Outcome, Verdict
45
+ from adl_compliance.policy.registry import load
46
+
47
+ ROW = load().by_id("REP-002")
48
+
49
+ requires_cargo = pytest.mark.skipif(shutil.which("cargo") is None, reason="cargo is not installed")
50
+
51
+ REGISTRY = "registry+https://github.com/rust-lang/crates.io-index"
52
+ HEADER = "# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\n"
53
+ FIX = (
54
+ "run: (cd backend && cargo update) to resync backend/Cargo.lock with "
55
+ "backend/Cargo.toml, then commit the result"
56
+ )
57
+
58
+
59
+ # ------------------------------------------------------------------- the fixtures
60
+
61
+
62
+ class Pkg(NamedTuple):
63
+ name: str
64
+ version: str = "1.0.0"
65
+ path: bool = False
66
+ """A path package (workspace member, path dependency): no `source` in the lock."""
67
+ needs: tuple[str, ...] = ()
68
+ """The strings of this package's `dependencies = [...]`, verbatim."""
69
+
70
+ def text(self) -> str:
71
+ lines = ["[[package]]", f'name = "{self.name}"', f'version = "{self.version}"']
72
+ if not self.path:
73
+ lines += [f'source = "{REGISTRY}"', f'checksum = "{"0" * 64}"']
74
+ if self.needs:
75
+ lines += ["dependencies = [", *(f' "{need}",' for need in self.needs), "]"]
76
+ return "\n".join(lines) + "\n"
77
+
78
+
79
+ def lock_text(packages: tuple[Pkg, ...], *, version: int | None = 3) -> str:
80
+ head = HEADER + (f"version = {version}\n" if version is not None else "")
81
+ return head + "\n" + "\n".join(pkg.text() for pkg in packages)
82
+
83
+
84
+ def without(packages: tuple[Pkg, ...], *names: str) -> tuple[Pkg, ...]:
85
+ """What `cargo update` would NOT write if `names` were never needed: the packages are gone
86
+ and no remaining package lists them either."""
87
+ return tuple(
88
+ p._replace(needs=tuple(n for n in p.needs if n.partition(" ")[0] not in names))
89
+ for p in packages
90
+ if p.name not in names
91
+ )
92
+
93
+
94
+ class Shape(NamedTuple):
95
+ """A manifest tree (paths relative to the cargo workspace) and the lock packages it needs.
96
+
97
+ A member's `needs` are the direct dependencies of its manifests, as cargo writes them."""
98
+
99
+ files: dict[str, str]
100
+ packages: tuple[Pkg, ...]
101
+
102
+ def lock(self, *, drop: tuple[str, ...] = (), swap: Mapping[str, Pkg] | None = None) -> str:
103
+ swap = swap or {}
104
+ kept = without(self.packages, *drop)
105
+ return lock_text(tuple(swap.get(p.name, p) for p in kept))
106
+
107
+
108
+ SINGLE = Shape(
109
+ files={
110
+ "Cargo.toml": """\
111
+ [package]
112
+ name = "app"
113
+ version = "0.1.0"
114
+ edition = "2021"
115
+
116
+ [dependencies]
117
+ serde = "1"
118
+ sj = { package = "serde_json_fork", version = "1" }
119
+ opt = { version = "1", optional = true }
120
+
121
+ [dev-dependencies]
122
+ pretty_assertions = "1"
123
+
124
+ [build-dependencies]
125
+ cc = "1"
126
+
127
+ [target.'cfg(windows)'.dependencies]
128
+ winapi = "0.3"
129
+
130
+ [target.'cfg(unix)'.build-dependencies]
131
+ pkg-config = "0.3"
132
+
133
+ [target.'cfg(unix)'.dev-dependencies]
134
+ tempfile = "3"
135
+ """
136
+ },
137
+ packages=(
138
+ Pkg(
139
+ "app",
140
+ "0.1.0",
141
+ path=True,
142
+ needs=(
143
+ "serde",
144
+ "serde_json_fork",
145
+ "opt",
146
+ "pretty_assertions",
147
+ "cc",
148
+ "winapi",
149
+ "pkg-config",
150
+ "tempfile",
151
+ ),
152
+ ),
153
+ Pkg("serde"),
154
+ Pkg("serde_json_fork"),
155
+ Pkg("opt"),
156
+ Pkg("pretty_assertions"),
157
+ Pkg("cc"),
158
+ Pkg("winapi", "0.3.9"),
159
+ Pkg("pkg-config", "0.3.30"),
160
+ Pkg("tempfile", "3.10.0"),
161
+ ),
162
+ )
163
+
164
+ # (real package name, the key it is declared under, the section it is declared in)
165
+ SINGLE_DEPENDENCIES = [
166
+ pytest.param("serde", "serde", "[dependencies]", id="normal"),
167
+ pytest.param("serde_json_fork", "sj", "[dependencies]", id="renamed via package="),
168
+ pytest.param("opt", "opt", "[dependencies]", id="optional"),
169
+ pytest.param("pretty_assertions", "pretty_assertions", "[dev-dependencies]", id="dev"),
170
+ pytest.param("cc", "cc", "[build-dependencies]", id="build"),
171
+ pytest.param("winapi", "winapi", "[target.'cfg(windows)'.dependencies]", id="target normal"),
172
+ pytest.param(
173
+ "pkg-config", "pkg-config", "[target.'cfg(unix)'.build-dependencies]", id="target build"
174
+ ),
175
+ pytest.param(
176
+ "tempfile", "tempfile", "[target.'cfg(unix)'.dev-dependencies]", id="target dev"
177
+ ),
178
+ ]
179
+
180
+ VIRTUAL = Shape(
181
+ files={
182
+ "Cargo.toml": """\
183
+ [workspace]
184
+ resolver = "2"
185
+ members = ["crates/*"]
186
+ exclude = ["crates/skipped"]
187
+
188
+ [workspace.package]
189
+ version = "0.3.0"
190
+ edition = "2021"
191
+
192
+ [workspace.dependencies]
193
+ serde = { version = "1", features = ["derive"] }
194
+ log = "0.4"
195
+ renamed = { package = "real_name", version = "2" }
196
+ core-lib = { path = "crates/core-lib" }
197
+ unused = "1"
198
+ """,
199
+ "crates/core-lib/Cargo.toml": """\
200
+ [package]
201
+ name = "core-lib"
202
+ version.workspace = true
203
+ edition.workspace = true
204
+ """,
205
+ "crates/web/Cargo.toml": """\
206
+ [package]
207
+ name = "web"
208
+ version = "0.1.0"
209
+ edition = "2021"
210
+
211
+ [dependencies]
212
+ serde.workspace = true
213
+ core-lib = { workspace = true }
214
+ renamed = { workspace = true, optional = true }
215
+
216
+ [dev-dependencies]
217
+ log = { workspace = true }
218
+ """,
219
+ "crates/skipped/Cargo.toml": """\
220
+ [package]
221
+ name = "skipped"
222
+ version = "9.9.9"
223
+ edition = "2021"
224
+
225
+ [dependencies]
226
+ only-skipped = "1"
227
+ """,
228
+ },
229
+ packages=(
230
+ Pkg("core-lib", "0.3.0", path=True),
231
+ Pkg("web", "0.1.0", path=True, needs=("serde", "core-lib", "real_name", "log")),
232
+ Pkg("serde"),
233
+ Pkg("log", "0.4.21"),
234
+ Pkg("real_name", "2.0.0"),
235
+ ),
236
+ )
237
+
238
+ # A root package that is also a workspace; `helper` is a path dependency living inside the
239
+ # root and listed nowhere in `members`: cargo makes it a member, so ITS dependencies are locked.
240
+ IMPLICIT = Shape(
241
+ files={
242
+ "Cargo.toml": """\
243
+ [package]
244
+ name = "root"
245
+ version = "0.1.0"
246
+ edition = "2021"
247
+
248
+ [workspace]
249
+ members = ["member-a"]
250
+
251
+ [dependencies]
252
+ h = { path = "helper", package = "helper" }
253
+ """,
254
+ "member-a/Cargo.toml": """\
255
+ [package]
256
+ name = "member-a"
257
+ version = "0.2.0"
258
+ edition = "2021"
259
+
260
+ [dependencies]
261
+ shared = { path = "../../shared" }
262
+ """,
263
+ "helper/Cargo.toml": """\
264
+ [package]
265
+ name = "helper"
266
+ version = "0.4.0"
267
+ edition = "2021"
268
+
269
+ [dependencies]
270
+ helper-dep = "1"
271
+ """,
272
+ "../shared/Cargo.toml": """\
273
+ [package]
274
+ name = "shared"
275
+ version = "0.5.0"
276
+ edition = "2021"
277
+ """,
278
+ },
279
+ packages=(
280
+ Pkg("root", "0.1.0", path=True, needs=("helper",)),
281
+ Pkg("member-a", "0.2.0", path=True, needs=("shared",)),
282
+ Pkg("helper", "0.4.0", path=True, needs=("helper-dep",)),
283
+ Pkg("shared", "0.5.0", path=True),
284
+ Pkg("helper-dep"),
285
+ ),
286
+ )
287
+
288
+ SHAPES = [
289
+ pytest.param(SINGLE, id="single package, every dependency table"),
290
+ pytest.param(VIRTUAL, id="virtual workspace, members glob + exclude + inheritance"),
291
+ pytest.param(IMPLICIT, id="root package and workspace, implicit path-dependency member"),
292
+ ]
293
+
294
+
295
+ def lay(root: Path, files: Mapping[str, str], lock: str | bytes, *, where: str = "backend") -> None:
296
+ """Write a cargo workspace under `root/where` plus the TypeScript lock the gate also wants.
297
+
298
+ A manifest with a `[package]` gets a `src/lib.rs`, so the very same files are also valid
299
+ input to the real `cargo metadata`. `where` is the cargo workspace directory.
300
+ """
301
+ base = root / where
302
+ for relative, body in files.items():
303
+ target = (base / relative).resolve()
304
+ target.parent.mkdir(parents=True, exist_ok=True)
305
+ target.write_text(body, encoding="utf-8")
306
+ if "[package]" in body:
307
+ (target.parent / "src").mkdir(exist_ok=True)
308
+ (target.parent / "src" / "lib.rs").write_text("", encoding="utf-8")
309
+ lockfile = base / "Cargo.lock"
310
+ if isinstance(lock, bytes):
311
+ lockfile.write_bytes(lock)
312
+ else:
313
+ lockfile.write_text(lock, encoding="utf-8")
314
+ (root / "frontend").mkdir(exist_ok=True)
315
+ (root / "frontend" / "bun.lock").write_text("# fixture lock\n", encoding="utf-8")
316
+
317
+
318
+ def grade(
319
+ root: Path, *, declarations: Mapping[str, str] | None = None, trees: Mapping[str, str] | None = None
320
+ ) -> Verdict:
321
+ """The real dispatch path, whole tree, exhaustive."""
322
+ trees = fixture_trees() if trees is None else trees
323
+ files = corpus_files(root)
324
+ return runner.run(
325
+ ROW,
326
+ root,
327
+ files,
328
+ exhaustive=True,
329
+ universe=files,
330
+ declarations=declarations or {},
331
+ trees=trees,
332
+ declared_trees=trees,
333
+ )
334
+
335
+
336
+ def messages(verdict: Verdict) -> list[str]:
337
+ return [finding.message for finding in verdict.findings]
338
+
339
+
340
+ def stale_lock(tmp_path: Path, files: Mapping[str, str], lock: str | bytes) -> Verdict:
341
+ lay(tmp_path, files, lock)
342
+ return grade(tmp_path)
343
+
344
+
345
+ @pytest.fixture
346
+ def cargo_accepts(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]:
347
+ """Both cargo calls succeed; returns every argv the check spawned."""
348
+ argvs: list[list[str]] = []
349
+
350
+ def fake_run(argv: list[str], *, cwd: Path, **_: object) -> object:
351
+ argvs.append(argv)
352
+ return SimpleNamespace(returncode=0)
353
+
354
+ monkeypatch.setattr(module.shutil, "which", lambda name: "/fake/cargo")
355
+ monkeypatch.setattr(module.subprocess, "run", fake_run)
356
+ return argvs
357
+
358
+
359
+ # --------------------------------------------- the three locks the previous probe measured
360
+
361
+ GARBAGE = [
362
+ pytest.param("version = 4\n", "backend/Cargo.lock lists no packages", id="version = 4 alone"),
363
+ pytest.param("this is [[ not a lockfile\n", "backend/Cargo.lock is not valid TOML", id="not TOML"),
364
+ pytest.param(
365
+ "version = 99\n", "backend/Cargo.lock version 99 is not supported", id="version = 99"
366
+ ),
367
+ ]
368
+
369
+
370
+ @pytest.mark.parametrize(("body", "expected"), GARBAGE)
371
+ def test_a_garbage_lock_is_lck_002(
372
+ tmp_path: Path, cargo_accepts: list[list[str]], body: str, expected: str
373
+ ) -> None:
374
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
375
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
376
+ assert {f.rule for f in verdict.findings} == {"LCK-002"}
377
+ assert {f.path for f in verdict.findings} == {"backend/Cargo.lock"}
378
+ assert any(message.startswith(expected) for message in messages(verdict)), messages(verdict)
379
+
380
+
381
+ @requires_cargo
382
+ @pytest.mark.parametrize(("body", "expected"), GARBAGE)
383
+ def test_a_garbage_lock_is_lck_002_with_the_real_cargo_command(
384
+ tmp_path: Path, body: str, expected: str
385
+ ) -> None:
386
+ """The very bodies the real `cargo metadata --no-deps` accepted: the gate now rejects them."""
387
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
388
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
389
+ assert {f.rule for f in verdict.findings} == {"LCK-002"}
390
+ assert any(message.startswith(expected) for message in messages(verdict)), messages(verdict)
391
+
392
+
393
+ def test_a_lock_that_is_not_utf8_is_lck_002(tmp_path: Path, cargo_accepts: list[list[str]]) -> None:
394
+ verdict = stale_lock(tmp_path, SINGLE.files, b"\xff\xfe\x00 not text \x80")
395
+ assert messages(verdict) == ["backend/Cargo.lock is not valid UTF-8 text"]
396
+
397
+
398
+ # ---------------------------------------------------------------- the lock's structure
399
+
400
+
401
+ @pytest.mark.parametrize("version", [3, 4, None], ids=["version = 3", "version = 4", "no version (V1/V2)"])
402
+ def test_a_supported_lock_version_is_clean(
403
+ tmp_path: Path, cargo_accepts: list[list[str]], version: int | None
404
+ ) -> None:
405
+ """cargo 0.99.0, `EncodableResolve::into_resolve`: `Some(4)`, `Some(3)` and, with no
406
+ `version` key at all, the legacy V1/V2 encodings are read; anything else is refused."""
407
+ body = lock_text(SINGLE.packages, version=version)
408
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
409
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
410
+
411
+
412
+ @pytest.mark.parametrize(
413
+ "line",
414
+ ["version = 1", "version = 2", "version = 5", "version = 0", "version = -3", "version = 99"],
415
+ )
416
+ def test_an_unsupported_integer_lock_version_is_reported(
417
+ tmp_path: Path, cargo_accepts: list[list[str]], line: str
418
+ ) -> None:
419
+ number = line.split("= ")[1]
420
+ body = HEADER + line + "\n\n" + "\n".join(p.text() for p in SINGLE.packages)
421
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
422
+ assert messages(verdict) == [
423
+ f"backend/Cargo.lock version {number} is not supported (cargo reads lock versions 3 and 4)"
424
+ ]
425
+
426
+
427
+ @pytest.mark.parametrize(
428
+ ("line", "shown"),
429
+ [('version = "4"', "'4'"), ("version = 4.0", "4.0"), ("version = true", "True")],
430
+ ids=["string", "float", "boolean"],
431
+ )
432
+ def test_a_lock_version_that_is_not_an_integer_is_reported(
433
+ tmp_path: Path, cargo_accepts: list[list[str]], line: str, shown: str
434
+ ) -> None:
435
+ body = HEADER + line + "\n\n" + "\n".join(p.text() for p in SINGLE.packages)
436
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
437
+ assert messages(verdict) == [
438
+ f"backend/Cargo.lock version {shown} is not supported (cargo reads lock versions 3 and 4)"
439
+ ]
440
+
441
+
442
+ @pytest.mark.parametrize(
443
+ "body",
444
+ [
445
+ pytest.param("version = 3\n", id="no [[package]]"),
446
+ pytest.param("version = 3\npackage = []\n", id="empty package array"),
447
+ ],
448
+ )
449
+ def test_a_lock_without_packages_is_reported(
450
+ tmp_path: Path, cargo_accepts: list[list[str]], body: str
451
+ ) -> None:
452
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
453
+ assert messages(verdict) == [
454
+ "backend/Cargo.lock lists no packages ([[package]] is absent or empty)"
455
+ ]
456
+
457
+
458
+ def test_an_empty_package_table_is_reported(tmp_path: Path, cargo_accepts: list[list[str]]) -> None:
459
+ body = "version = 3\n\n[[package]]\n"
460
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
461
+ assert messages(verdict) == [
462
+ "backend/Cargo.lock has a [[package]] entry (number 1) without a string name"
463
+ ]
464
+
465
+
466
+ @pytest.mark.parametrize(
467
+ ("entry", "expected"),
468
+ [
469
+ pytest.param('version = "1.0.0"\n', "without a string name", id="no name"),
470
+ pytest.param('name = "x"\n', "without a string version", id="no version"),
471
+ pytest.param('name = 7\nversion = "1.0.0"\n', "without a string name", id="name not a string"),
472
+ pytest.param('name = "x"\nversion = 1\n', "without a string version", id="version not a string"),
473
+ ],
474
+ )
475
+ def test_a_package_entry_without_name_or_version_is_reported(
476
+ tmp_path: Path, cargo_accepts: list[list[str]], entry: str, expected: str
477
+ ) -> None:
478
+ good = "\n".join(p.text() for p in SINGLE.packages)
479
+ body = HEADER + "version = 3\n\n" + good + "\n[[package]]\n" + entry
480
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
481
+ number = len(SINGLE.packages) + 1
482
+ assert messages(verdict) == [f"backend/Cargo.lock has a [[package]] entry (number {number}) {expected}"]
483
+
484
+
485
+ def test_a_package_entry_that_is_not_a_table_is_reported(
486
+ tmp_path: Path, cargo_accepts: list[list[str]]
487
+ ) -> None:
488
+ verdict = stale_lock(tmp_path, SINGLE.files, "version = 3\npackage = [1]\n")
489
+ assert messages(verdict) == [
490
+ "backend/Cargo.lock has a [[package]] entry (number 1) that is not a table"
491
+ ]
492
+
493
+
494
+ # ------------------------------------- the lock must hold every package it says it needs
495
+
496
+ # The shapes cargo writes `dependencies = [...]` in (V1 to V4): a bare name when only one
497
+ # version of it is locked, `name version` when several are, `name version (source)` when
498
+ # several sources are. A package that was removed from the lock but is still listed by a
499
+ # dependent is a lock cargo refuses to parse, and it passed too.
500
+ CLOSED = (
501
+ Pkg("app", "0.1.0", path=True, needs=("serde", "twice 1.0.0", f"other 2.0.0 ({REGISTRY})")),
502
+ Pkg("serde", needs=("transitive",)),
503
+ Pkg("transitive"),
504
+ Pkg("twice", "1.0.0"),
505
+ Pkg("twice", "2.0.0"),
506
+ Pkg("other", "2.0.0"),
507
+ )
508
+
509
+
510
+ def test_a_lock_closed_over_its_own_dependencies_is_clean(
511
+ tmp_path: Path, cargo_accepts: list[list[str]]
512
+ ) -> None:
513
+ manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
514
+ verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, lock_text(CLOSED))
515
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
516
+
517
+
518
+ @pytest.mark.parametrize(
519
+ ("removed", "listed"),
520
+ [
521
+ pytest.param("transitive", "transitive", id="a transitive package removed"),
522
+ pytest.param("other", f"other 2.0.0 ({REGISTRY})", id="a package named with its source removed"),
523
+ ],
524
+ )
525
+ def test_a_package_the_lock_lists_as_a_dependency_but_does_not_hold_is_reported(
526
+ tmp_path: Path, cargo_accepts: list[list[str]], removed: str, listed: str
527
+ ) -> None:
528
+ manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
529
+ body = lock_text(tuple(p for p in CLOSED if p.name != removed))
530
+ verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, body)
531
+ [message] = messages(verdict)
532
+ assert message.startswith(f"backend/Cargo.lock lists dependency '{listed}' of package '")
533
+ assert message.endswith("but has no such package")
534
+
535
+
536
+ def test_a_dependency_naming_a_version_the_lock_does_not_hold_is_reported(
537
+ tmp_path: Path, cargo_accepts: list[list[str]]
538
+ ) -> None:
539
+ manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
540
+ body = lock_text(tuple(p for p in CLOSED if (p.name, p.version) != ("twice", "1.0.0")))
541
+ verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, body)
542
+ assert messages(verdict) == [
543
+ "backend/Cargo.lock lists dependency 'twice 1.0.0' of package 'app' 0.1.0 but has no such package"
544
+ ]
545
+
546
+
547
+ def test_a_missing_package_listed_by_two_dependents_is_reported_once(
548
+ tmp_path: Path, cargo_accepts: list[list[str]]
549
+ ) -> None:
550
+ manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
551
+ packages = (
552
+ Pkg("app", "0.1.0", path=True, needs=("serde", "other")),
553
+ Pkg("serde", needs=("ghost",)),
554
+ Pkg("other", needs=("ghost",)),
555
+ )
556
+ verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, lock_text(packages))
557
+ assert len(messages(verdict)) == 1, messages(verdict)
558
+ assert "lists dependency 'ghost'" in messages(verdict)[0]
559
+
560
+
561
+ def test_a_dependencies_entry_that_is_not_a_list_of_strings_is_reported(
562
+ tmp_path: Path, cargo_accepts: list[list[str]]
563
+ ) -> None:
564
+ body = HEADER + 'version = 3\n\n[[package]]\nname = "app"\nversion = "0.1.0"\ndependencies = "serde"\n'
565
+ verdict = stale_lock(tmp_path, SINGLE.files, body)
566
+ assert messages(verdict) == [
567
+ "backend/Cargo.lock has a [[package]] entry (number 1) whose `dependencies` is not a list of strings"
568
+ ]
569
+
570
+
571
+ # --------------------------------------------------------------------- good locks pass
572
+
573
+
574
+ @pytest.mark.parametrize("shape", SHAPES)
575
+ def test_a_lock_that_satisfies_the_manifests_is_clean(
576
+ tmp_path: Path, cargo_accepts: list[list[str]], shape: Shape
577
+ ) -> None:
578
+ verdict = stale_lock(tmp_path, shape.files, shape.lock())
579
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
580
+ assert verdict.findings == ()
581
+
582
+
583
+ @requires_cargo
584
+ @pytest.mark.parametrize("shape", SHAPES)
585
+ def test_a_lock_that_satisfies_the_manifests_is_clean_with_the_real_cargo_command(
586
+ tmp_path: Path, shape: Shape
587
+ ) -> None:
588
+ verdict = stale_lock(tmp_path, shape.files, shape.lock())
589
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
590
+
591
+
592
+ # -------------------------------------------------- a direct dependency missing is found
593
+
594
+
595
+ @pytest.mark.parametrize(("name", "key", "section"), SINGLE_DEPENDENCIES)
596
+ def test_a_direct_dependency_missing_from_the_lock_is_reported(
597
+ tmp_path: Path, cargo_accepts: list[list[str]], name: str, key: str, section: str
598
+ ) -> None:
599
+ verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=(name,)))
600
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
601
+ assert messages(verdict) == [
602
+ f"backend/Cargo.lock lists no package '{name}' "
603
+ f"(dependency '{key}' in {section} of backend/Cargo.toml)"
604
+ ]
605
+
606
+
607
+ @requires_cargo
608
+ @pytest.mark.parametrize(("name", "key", "section"), SINGLE_DEPENDENCIES)
609
+ def test_a_direct_dependency_missing_from_the_lock_is_reported_with_the_real_cargo_command(
610
+ tmp_path: Path, name: str, key: str, section: str
611
+ ) -> None:
612
+ verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=(name,)))
613
+ assert [f.rule for f in verdict.findings] == ["LCK-002"], verdict.render()
614
+ assert f"lists no package '{name}'" in verdict.findings[0].message
615
+
616
+
617
+ @pytest.mark.parametrize(("name", "key", "section"), SINGLE_DEPENDENCIES)
618
+ def test_a_dependency_the_lock_holds_only_for_someone_else_is_reported(
619
+ tmp_path: Path, cargo_accepts: list[list[str]], name: str, key: str, section: str
620
+ ) -> None:
621
+ """The commonest stale lock: a manifest gains a dependency that is already in the lock because
622
+ another package brings it (`tokio`, `serde`). The package is in the lock; the MEMBER's own entry
623
+ does not list it, so `cargo build --locked` would refuse the lock. Presence anywhere is not enough."""
624
+ app, *rest = SINGLE.packages
625
+ app = app._replace(needs=tuple(n for n in app.needs if n != name))
626
+ carrier = Pkg("carrier", needs=(name,))
627
+ verdict = stale_lock(tmp_path, SINGLE.files, lock_text((app, *rest, carrier)))
628
+ assert messages(verdict) == [
629
+ f"backend/Cargo.lock package 'app' 0.1.0 does not list '{name}' as a dependency "
630
+ f"({section} of backend/Cargo.toml declares dependency '{key}')"
631
+ ]
632
+
633
+
634
+ def test_a_workspace_member_whose_entry_lacks_an_inherited_dependency_is_reported(
635
+ tmp_path: Path, cargo_accepts: list[list[str]]
636
+ ) -> None:
637
+ web = next(p for p in VIRTUAL.packages if p.name == "web")
638
+ packages = tuple(
639
+ p._replace(needs=tuple(n for n in p.needs if n != "serde")) if p is web else p
640
+ for p in VIRTUAL.packages
641
+ )
642
+ packages = (*packages, Pkg("carrier", needs=("serde",)))
643
+ verdict = stale_lock(tmp_path, VIRTUAL.files, lock_text(packages))
644
+ assert messages(verdict) == [
645
+ "backend/Cargo.lock package 'web' 0.1.0 does not list 'serde' as a dependency "
646
+ "([dependencies] of backend/crates/web/Cargo.toml declares dependency 'serde')"
647
+ ]
648
+
649
+
650
+ def test_a_dependency_listed_with_its_version_is_found_by_name(
651
+ tmp_path: Path, cargo_accepts: list[list[str]]
652
+ ) -> None:
653
+ """cargo writes `name version` when two versions of a crate are locked."""
654
+ app, *rest = SINGLE.packages
655
+ app = app._replace(needs=tuple("serde 1.0.0" if n == "serde" else n for n in app.needs))
656
+ packages = (app, *rest, Pkg("serde", "2.0.0"))
657
+ verdict = stale_lock(tmp_path, SINGLE.files, lock_text(packages))
658
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
659
+
660
+
661
+ def test_a_package_that_dev_depends_on_itself_is_not_asked_to_list_itself(
662
+ tmp_path: Path, cargo_accepts: list[list[str]]
663
+ ) -> None:
664
+ manifest = (
665
+ '[package]\nname = "app"\nversion = "0.1.0"\n\n[features]\nextra = []\n\n'
666
+ '[dev-dependencies]\napp = { path = ".", features = ["extra"] }\n'
667
+ )
668
+ verdict = stale_lock(
669
+ tmp_path, {"Cargo.toml": manifest}, lock_text((Pkg("app", "0.1.0", path=True),))
670
+ )
671
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
672
+
673
+
674
+ def test_a_renamed_dependency_is_judged_by_its_real_name_not_its_key(
675
+ tmp_path: Path, cargo_accepts: list[list[str]]
676
+ ) -> None:
677
+ """`sj = { package = "serde_json_fork" }`: a lock listing a package called `sj` is NOT
678
+ the dependency, and one listing `serde_json_fork` is, with no `sj` anywhere."""
679
+ wrong = SINGLE.lock(drop=("serde_json_fork",)) + Pkg("sj").text()
680
+ verdict = stale_lock(tmp_path, SINGLE.files, wrong)
681
+ assert [m.split(" (")[0] for m in messages(verdict)] == [
682
+ "backend/Cargo.lock lists no package 'serde_json_fork'"
683
+ ]
684
+
685
+
686
+ def test_a_dependency_declared_as_its_own_table_is_judged(
687
+ tmp_path: Path, cargo_accepts: list[list[str]]
688
+ ) -> None:
689
+ manifest = (
690
+ '[package]\nname = "app"\nversion = "0.1.0"\n\n'
691
+ '[dependencies.tokio]\nversion = "1"\nfeatures = ["rt"]\n\n'
692
+ '[dev_dependencies]\nmockito = "1"\n\n'
693
+ '[build_dependencies]\nprost-build = "0.12"\n'
694
+ )
695
+ packages = (
696
+ Pkg("app", "0.1.0", path=True, needs=("tokio", "mockito", "prost-build")),
697
+ Pkg("tokio"),
698
+ Pkg("mockito"),
699
+ Pkg("prost-build"),
700
+ )
701
+ clean = stale_lock(tmp_path, {"Cargo.toml": manifest}, lock_text(packages))
702
+ assert clean.outcome is Outcome.CLEAN, clean.render()
703
+ gone = grade_without(tmp_path, packages, "tokio")
704
+ assert [m.split(" (")[0] for m in messages(gone)] == ["backend/Cargo.lock lists no package 'tokio'"]
705
+ gone = grade_without(tmp_path, packages, "mockito")
706
+ assert "in [dev_dependencies] of backend/Cargo.toml" in messages(gone)[0]
707
+ gone = grade_without(tmp_path, packages, "prost-build")
708
+ assert "in [build_dependencies] of backend/Cargo.toml" in messages(gone)[0]
709
+
710
+
711
+ def grade_without(root: Path, packages: tuple[Pkg, ...], name: str) -> Verdict:
712
+ (root / "backend" / "Cargo.lock").write_text(lock_text(without(packages, name)), encoding="utf-8")
713
+ return grade(root)
714
+
715
+
716
+ def test_a_dependency_both_members_need_is_reported_once(
717
+ tmp_path: Path, cargo_accepts: list[list[str]]
718
+ ) -> None:
719
+ files = {
720
+ **VIRTUAL.files,
721
+ "crates/api/Cargo.toml": '[package]\nname = "api"\nversion = "0.1.0"\n\n[dependencies]\nserde.workspace = true\n',
722
+ }
723
+ packages = (*VIRTUAL.packages, Pkg("api", "0.1.0", path=True, needs=("serde",)))
724
+ body = lock_text(without(packages, "serde"))
725
+ verdict = stale_lock(tmp_path, files, body)
726
+ assert len(messages(verdict)) == 1, messages(verdict)
727
+ assert messages(verdict)[0].startswith("backend/Cargo.lock lists no package 'serde' (dependency 'serde' in [dependencies] of ")
728
+
729
+
730
+ # ------------------------------------------------------------ a workspace member missing
731
+
732
+
733
+ def test_the_root_package_missing_from_the_lock_is_reported(
734
+ tmp_path: Path, cargo_accepts: list[list[str]]
735
+ ) -> None:
736
+ verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=("app",)))
737
+ assert messages(verdict) == [
738
+ "backend/Cargo.lock lists no path package 'app' (workspace member backend/Cargo.toml)"
739
+ ]
740
+
741
+
742
+ def test_a_member_whose_version_differs_is_reported(
743
+ tmp_path: Path, cargo_accepts: list[list[str]]
744
+ ) -> None:
745
+ verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(swap={"app": Pkg("app", "0.0.9", path=True)}))
746
+ assert messages(verdict) == [
747
+ "backend/Cargo.lock lists 'app' at version 0.0.9 but workspace member "
748
+ "backend/Cargo.toml declares 0.1.0"
749
+ ]
750
+
751
+
752
+ def test_a_registry_package_of_the_same_name_does_not_satisfy_a_member(
753
+ tmp_path: Path, cargo_accepts: list[list[str]]
754
+ ) -> None:
755
+ """A member is a path package; a crates.io `app 0.1.0` in the lock is somebody else's."""
756
+ verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(swap={"app": Pkg("app", "0.1.0")}))
757
+ assert messages(verdict) == [
758
+ "backend/Cargo.lock lists no path package 'app' (workspace member backend/Cargo.toml)"
759
+ ]
760
+
761
+
762
+ @pytest.mark.parametrize("member", ["web", "core-lib"])
763
+ def test_a_virtual_workspace_member_missing_from_the_lock_is_reported(
764
+ tmp_path: Path, cargo_accepts: list[list[str]], member: str
765
+ ) -> None:
766
+ """One finding: `core-lib` is also a path dependency of `web`, and is reported once, as the member."""
767
+ verdict = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock(drop=(member,)))
768
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
769
+ assert messages(verdict) == [
770
+ f"backend/Cargo.lock lists no path package '{member}' "
771
+ f"(workspace member backend/crates/{member}/Cargo.toml)"
772
+ ]
773
+
774
+
775
+ def test_a_member_version_inherited_from_the_workspace_is_the_one_compared(
776
+ tmp_path: Path, cargo_accepts: list[list[str]]
777
+ ) -> None:
778
+ """`version.workspace = true` reads `[workspace.package].version` (0.3.0)."""
779
+ verdict = stale_lock(
780
+ tmp_path, VIRTUAL.files, VIRTUAL.lock(swap={"core-lib": Pkg("core-lib", "0.2.0", path=True)})
781
+ )
782
+ assert messages(verdict) == [
783
+ "backend/Cargo.lock lists 'core-lib' at version 0.2.0 but workspace member "
784
+ "backend/crates/core-lib/Cargo.toml declares 0.3.0"
785
+ ]
786
+
787
+
788
+ def test_a_member_with_no_version_is_locked_at_0_0_0(
789
+ tmp_path: Path, cargo_accepts: list[list[str]]
790
+ ) -> None:
791
+ files = {"Cargo.toml": '[package]\nname = "app"\nedition = "2021"\n'}
792
+ clean = stale_lock(tmp_path, files, lock_text((Pkg("app", "0.0.0", path=True),)))
793
+ assert clean.outcome is Outcome.CLEAN, clean.render()
794
+ wrong = stale_lock(tmp_path, files, lock_text((Pkg("app", "0.1.0", path=True),)))
795
+ assert "declares 0.0.0" in messages(wrong)[0]
796
+
797
+
798
+ # --------------------------------------------------- members, glob, exclude, inheritance
799
+
800
+
801
+ def test_the_excluded_member_is_not_required_and_removing_the_exclude_requires_it(
802
+ tmp_path: Path, cargo_accepts: list[list[str]]
803
+ ) -> None:
804
+ """Negative control for `exclude`: `skipped` (and its `only-skipped` dependency) are in no
805
+ lock above and the gate is clean; take the `exclude` away and `skipped` becomes a member."""
806
+ clean = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock())
807
+ assert clean.outcome is Outcome.CLEAN, clean.render()
808
+
809
+ unexcluded = {
810
+ **VIRTUAL.files,
811
+ "Cargo.toml": VIRTUAL.files["Cargo.toml"].replace('exclude = ["crates/skipped"]\n', ""),
812
+ }
813
+ verdict = stale_lock(tmp_path, unexcluded, VIRTUAL.lock())
814
+ found = messages(verdict)
815
+ assert any("lists no path package 'skipped'" in m for m in found), found
816
+ assert any("lists no package 'only-skipped'" in m for m in found), found
817
+
818
+
819
+ def test_a_workspace_dependency_no_member_uses_is_not_required(
820
+ tmp_path: Path, cargo_accepts: list[list[str]]
821
+ ) -> None:
822
+ """`[workspace.dependencies] unused = "1"`: cargo locks only what a member uses."""
823
+ assert "unused" in VIRTUAL.files["Cargo.toml"]
824
+ verdict = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock())
825
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
826
+
827
+
828
+ @pytest.mark.parametrize(
829
+ ("name", "key", "section"),
830
+ [
831
+ pytest.param("serde", "serde", "[dependencies]", id="inherited via workspace = true"),
832
+ pytest.param("real_name", "renamed", "[dependencies]", id="inherited, renamed, optional"),
833
+ pytest.param("log", "log", "[dev-dependencies]", id="inherited dev"),
834
+ ],
835
+ )
836
+ def test_an_inherited_dependency_missing_from_the_lock_is_reported(
837
+ tmp_path: Path, cargo_accepts: list[list[str]], name: str, key: str, section: str
838
+ ) -> None:
839
+ verdict = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock(drop=(name,)))
840
+ assert messages(verdict) == [
841
+ f"backend/Cargo.lock lists no package '{name}' (dependency '{key}' in {section} of "
842
+ "backend/crates/web/Cargo.toml)"
843
+ ]
844
+
845
+
846
+ def test_members_listed_without_a_glob_are_judged(
847
+ tmp_path: Path, cargo_accepts: list[list[str]]
848
+ ) -> None:
849
+ files = {
850
+ **VIRTUAL.files,
851
+ "Cargo.toml": VIRTUAL.files["Cargo.toml"].replace(
852
+ 'members = ["crates/*"]', 'members = ["crates/web", "crates/core-lib"]'
853
+ ),
854
+ }
855
+ clean = stale_lock(tmp_path, files, VIRTUAL.lock())
856
+ assert clean.outcome is Outcome.CLEAN, clean.render()
857
+ gone = stale_lock(tmp_path, files, VIRTUAL.lock(drop=("web",)))
858
+ assert any("lists no path package 'web'" in m for m in messages(gone))
859
+
860
+
861
+ def test_a_hidden_directory_matched_by_a_glob_is_a_member(
862
+ tmp_path: Path, cargo_accepts: list[list[str]]
863
+ ) -> None:
864
+ """Rust's `glob` crate (what cargo expands `members` with) has
865
+ `require_literal_leading_dot = false` by default: `crates/*` matches `crates/.tools`."""
866
+ files = {
867
+ **VIRTUAL.files,
868
+ "crates/.tools/Cargo.toml": '[package]\nname = "tools"\nversion = "0.1.0"\n',
869
+ }
870
+ verdict = stale_lock(tmp_path, files, VIRTUAL.lock())
871
+ assert any("lists no path package 'tools'" in m for m in messages(verdict)), messages(verdict)
872
+
873
+
874
+ # ------------------------------------------------------------------------- path dependencies
875
+
876
+
877
+ @pytest.mark.parametrize(
878
+ ("missing", "expected"),
879
+ [
880
+ pytest.param("helper-dep", "lists no package 'helper-dep' (dependency 'helper-dep' in [dependencies] of backend/helper/Cargo.toml)", id="dependency of an implicit member"),
881
+ pytest.param("helper", "lists no path package 'helper'", id="the implicit member itself, under its real name"),
882
+ pytest.param("shared", "lists no path package 'shared' (dependency 'shared' in [dependencies] of backend/member-a/Cargo.toml)", id="path dependency outside the workspace root"),
883
+ ],
884
+ )
885
+ def test_path_dependencies_are_judged_by_the_name_in_their_own_manifest(
886
+ tmp_path: Path, cargo_accepts: list[list[str]], missing: str, expected: str
887
+ ) -> None:
888
+ """`h = { path = "helper", package = "helper" }` and `shared = { path = "../../shared" }`:
889
+ the lock names the package its manifest declares. `helper` lives inside the workspace root
890
+ and is in no `members` list, so cargo makes it a member and locks its dependencies."""
891
+ verdict = stale_lock(tmp_path, IMPLICIT.files, IMPLICIT.lock(drop=(missing,)))
892
+ assert any(expected in m for m in messages(verdict)), messages(verdict)
893
+
894
+
895
+ def test_a_path_dependency_keyed_differently_from_its_package_is_judged_by_the_package(
896
+ tmp_path: Path, cargo_accepts: list[list[str]]
897
+ ) -> None:
898
+ files = {
899
+ "Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nalias = { path = "real" }\n',
900
+ "real/Cargo.toml": '[package]\nname = "real-name"\nversion = "0.7.0"\n',
901
+ }
902
+ good = (Pkg("app", "0.1.0", path=True, needs=("real-name",)), Pkg("real-name", "0.7.0", path=True))
903
+ clean = stale_lock(tmp_path, files, lock_text(good))
904
+ assert clean.outcome is Outcome.CLEAN, clean.render()
905
+ keyed = (Pkg("app", "0.1.0", path=True, needs=("alias",)), Pkg("alias", "0.7.0", path=True))
906
+ wrong = stale_lock(tmp_path, files, lock_text(keyed))
907
+ assert [m.split(" (")[0] for m in messages(wrong)] == ["backend/Cargo.lock lists no path package 'real-name'"]
908
+
909
+
910
+ def test_a_registry_package_does_not_satisfy_an_implicit_member(
911
+ tmp_path: Path, cargo_accepts: list[list[str]]
912
+ ) -> None:
913
+ verdict = stale_lock(
914
+ tmp_path, IMPLICIT.files, IMPLICIT.lock(swap={"helper": Pkg("helper", "0.4.0")})
915
+ )
916
+ assert any("lists no path package 'helper'" in m for m in messages(verdict)), messages(verdict)
917
+
918
+
919
+ def test_a_registry_package_does_not_satisfy_a_path_dependency_outside_the_workspace(
920
+ tmp_path: Path, cargo_accepts: list[list[str]]
921
+ ) -> None:
922
+ """`shared` is a path dependency and no member: only the dependency check can notice that the
923
+ lock holds a crates.io `shared 0.5.0` instead of the path package."""
924
+ verdict = stale_lock(
925
+ tmp_path, IMPLICIT.files, IMPLICIT.lock(swap={"shared": Pkg("shared", "0.5.0")})
926
+ )
927
+ assert messages(verdict) == [
928
+ "backend/Cargo.lock lists no path package 'shared' (dependency 'shared' in "
929
+ "[dependencies] of backend/member-a/Cargo.toml)"
930
+ ]
931
+
932
+
933
+ # --------------------------------------------------------- what the check cannot judge
934
+
935
+
936
+ UNJUDGEABLE = [
937
+ pytest.param(
938
+ {"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = 3\n'},
939
+ ["'foo'", "backend/Cargo.toml", "[dependencies]"],
940
+ id="dependency spec that is neither a string nor a table",
941
+ ),
942
+ pytest.param(
943
+ {"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = { workspace = false }\n'},
944
+ ["'foo'", "workspace = false"],
945
+ id="workspace = false",
946
+ ),
947
+ pytest.param(
948
+ {"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = { workspace = true }\n'},
949
+ ["'foo'", "[workspace.dependencies]"],
950
+ id="workspace = true without a [workspace.dependencies] entry",
951
+ ),
952
+ pytest.param(
953
+ {"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = { path = "nowhere" }\n'},
954
+ ["'foo'", "nowhere"],
955
+ id="path dependency whose manifest is not there",
956
+ ),
957
+ pytest.param(
958
+ {"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[target.\'cfg(unix)\'.dev-dependencies]\nbar = [1]\n'},
959
+ ["'bar'", "[target.'cfg(unix)'.dev-dependencies]"],
960
+ id="target-specific dependency spec that is a list",
961
+ ),
962
+ pytest.param(
963
+ {"Cargo.toml": 'dependencies = 3\n\n[package]\nname = "app"\nversion = "0.1.0"\n'},
964
+ ["[dependencies]", "backend/Cargo.toml", "not a table"],
965
+ id="a dependency section that is not a table",
966
+ ),
967
+ pytest.param(
968
+ {"Cargo.toml": '[workspace]\nmembers = ["crates/none-*"]\n'},
969
+ ["crates/none-*", "matches no directory"],
970
+ id="members entry that matches nothing",
971
+ ),
972
+ pytest.param(
973
+ {"Cargo.toml": '[workspace]\nmembers = ["crates/a"]\n', "crates/a/readme.txt": "no manifest here\n"},
974
+ ["crates/a", "no Cargo.toml"],
975
+ id="member directory without a Cargo.toml",
976
+ ),
977
+ pytest.param(
978
+ {"Cargo.toml": '[workspace]\nmembers = ["crates/*"]\nexclude = ["crates/g*"]\n', "crates/a/Cargo.toml": '[package]\nname = "a"\nversion = "0.1.0"\n'},
979
+ ["exclude", "crates/g*"],
980
+ id="exclude entry with glob syntax",
981
+ ),
982
+ pytest.param(
983
+ {"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n[dependencies\n'},
984
+ ["backend/Cargo.toml", "not valid TOML"],
985
+ id="manifest that is not TOML",
986
+ ),
987
+ pytest.param(
988
+ {"Cargo.toml": '[profile.release]\nlto = true\n'},
989
+ ["backend/Cargo.toml", "[package]", "[workspace]"],
990
+ id="manifest with neither [package] nor [workspace]",
991
+ ),
992
+ pytest.param(
993
+ {"Cargo.toml": '[workspace]\nmembers = ["crates/a"]\n', "crates/a/Cargo.toml": '[workspace]\nmembers = []\n'},
994
+ ["crates/a/Cargo.toml", "[package]"],
995
+ id="a member that is not a package",
996
+ ),
997
+ pytest.param(
998
+ {"Cargo.toml": '[package]\nname = "app"\nversion = 5\n'},
999
+ ["backend/Cargo.toml", "version"],
1000
+ id="package version that is not a string",
1001
+ ),
1002
+ ]
1003
+
1004
+
1005
+ @pytest.mark.parametrize(("files", "named"), UNJUDGEABLE)
1006
+ def test_what_the_manifests_text_cannot_judge_is_could_not_run_naming_it(
1007
+ tmp_path: Path, cargo_accepts: list[list[str]], files: dict[str, str], named: list[str]
1008
+ ) -> None:
1009
+ """Never skipped, never a pass: the lock below is fine, and the verdict is still could-not-run."""
1010
+ verdict = stale_lock(tmp_path, files, lock_text((Pkg("app", "0.1.0", path=True), Pkg("a", "0.1.0", path=True))))
1011
+ assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
1012
+ assert verdict.findings == ()
1013
+ for fragment in named:
1014
+ assert fragment in verdict.message, (fragment, verdict.message)
1015
+
1016
+
1017
+ def test_a_garbage_lock_is_reported_even_when_the_manifests_cannot_be_judged(
1018
+ tmp_path: Path, cargo_accepts: list[list[str]]
1019
+ ) -> None:
1020
+ """The lock alone is conclusive: it need not be compared with anything."""
1021
+ verdict = stale_lock(tmp_path, {"Cargo.toml": "[profile.release]\n"}, "version = 99\n")
1022
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
1023
+
1024
+
1025
+ # ----------------------------------------------- the finding's wording, remedy and modes
1026
+
1027
+
1028
+ def test_the_finding_wording_and_remedy(tmp_path: Path, cargo_accepts: list[list[str]]) -> None:
1029
+ verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=("serde",)))
1030
+ [finding] = verdict.findings
1031
+ assert finding.rule == "LCK-002"
1032
+ assert finding.path == "backend/Cargo.lock"
1033
+ assert finding.message == (
1034
+ "backend/Cargo.lock lists no package 'serde' (dependency 'serde' in [dependencies] of backend/Cargo.toml)"
1035
+ )
1036
+ assert finding.fix == FIX
1037
+
1038
+
1039
+ def test_the_remedy_is_the_one_a_cargo_rejection_has(
1040
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1041
+ ) -> None:
1042
+ """One remedy for every LCK-002 message, cargo's own rejection included."""
1043
+ monkeypatch.setattr(module.shutil, "which", lambda name: "/fake/cargo")
1044
+ monkeypatch.setattr(
1045
+ module.subprocess,
1046
+ "run",
1047
+ lambda argv, **_: SimpleNamespace(returncode=0 if argv[1] == "--version" else 101),
1048
+ )
1049
+ verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock())
1050
+ [finding] = verdict.findings
1051
+ assert finding.message == "backend/Cargo.lock does not satisfy backend/Cargo.toml"
1052
+ assert finding.fix == FIX
1053
+
1054
+
1055
+ def test_a_cargo_rejection_is_the_only_finding_for_that_lock(
1056
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1057
+ ) -> None:
1058
+ """The early-return shape: a manifest cargo refuses is one finding. A garbage lock beside
1059
+ it is not reported on top (cargo is the authority on the manifest, and the lock cannot be
1060
+ compared with a manifest cargo did not accept)."""
1061
+ calls: list[list[str]] = []
1062
+
1063
+ def fake_run(argv: list[str], **_: object) -> object:
1064
+ calls.append(argv)
1065
+ return SimpleNamespace(returncode=0 if argv[1] == "--version" else 101)
1066
+
1067
+ monkeypatch.setattr(module.shutil, "which", lambda name: "/fake/cargo")
1068
+ monkeypatch.setattr(module.subprocess, "run", fake_run)
1069
+ verdict = stale_lock(tmp_path, SINGLE.files, "this is [[ not a lockfile\n")
1070
+ assert messages(verdict) == ["backend/Cargo.lock does not satisfy backend/Cargo.toml"]
1071
+ assert [argv[:2] for argv in calls] == [["cargo", "--version"], ["cargo", "metadata"]]
1072
+
1073
+
1074
+ def test_the_undeclared_mode_grades_the_tree_default_lockfile(
1075
+ tmp_path: Path, cargo_accepts: list[list[str]]
1076
+ ) -> None:
1077
+ verdict = stale_lock(tmp_path, SINGLE.files, "version = 99\n")
1078
+ assert {f.path for f in verdict.findings} == {"backend/Cargo.lock"}
1079
+ assert {f.fix for f in verdict.findings} == {FIX}
1080
+
1081
+
1082
+ DECLARED = {"cargo-manifests": "nested/*/Cargo.toml", "cargo-lockfiles": "nested/ws/Cargo.lock"}
1083
+
1084
+
1085
+ def test_the_declared_mode_grades_the_declared_lockfile_in_its_own_directory(
1086
+ tmp_path: Path, cargo_accepts: list[list[str]]
1087
+ ) -> None:
1088
+ lay(tmp_path, SINGLE.files, SINGLE.lock(drop=("cc",)), where="nested/ws")
1089
+ verdict = grade(tmp_path, declarations=DECLARED)
1090
+ [finding] = verdict.findings
1091
+ assert finding.rule == "LCK-002"
1092
+ assert finding.path == "nested/ws/Cargo.lock"
1093
+ assert finding.message == (
1094
+ "nested/ws/Cargo.lock lists no package 'cc' "
1095
+ "(dependency 'cc' in [build-dependencies] of nested/ws/Cargo.toml)"
1096
+ )
1097
+ assert finding.fix == (
1098
+ "run: (cd nested/ws && cargo update) to resync nested/ws/Cargo.lock with "
1099
+ "nested/ws/Cargo.toml, then commit the result"
1100
+ )
1101
+
1102
+
1103
+ def test_the_declared_mode_is_clean_for_a_good_lock(
1104
+ tmp_path: Path, cargo_accepts: list[list[str]]
1105
+ ) -> None:
1106
+ lay(tmp_path, SINGLE.files, SINGLE.lock(), where="nested/ws")
1107
+ verdict = grade(tmp_path, declarations=DECLARED)
1108
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
1109
+
1110
+
1111
+ def test_every_declared_lockfile_is_judged_and_each_stale_one_is_reported(
1112
+ tmp_path: Path, cargo_accepts: list[list[str]]
1113
+ ) -> None:
1114
+ lay(tmp_path, SINGLE.files, SINGLE.lock(drop=("cc",)), where="nested/a")
1115
+ lay(tmp_path, SINGLE.files, SINGLE.lock(), where="nested/b")
1116
+ lay(tmp_path, SINGLE.files, "version = 99\n", where="nested/c")
1117
+ declarations = {
1118
+ "cargo-manifests": "nested/*/Cargo.toml",
1119
+ "cargo-lockfiles": "nested/a/Cargo.lock,nested/b/Cargo.lock,nested/c/Cargo.lock",
1120
+ }
1121
+ verdict = grade(tmp_path, declarations=declarations)
1122
+ assert sorted({f.path for f in verdict.findings}) == ["nested/a/Cargo.lock", "nested/c/Cargo.lock"]
1123
+
1124
+
1125
+ # --------------------------------------------- determinism: cargo is asked only what it was
1126
+
1127
+
1128
+ def test_cargo_is_asked_exactly_the_two_questions_it_always_was(
1129
+ tmp_path: Path, cargo_accepts: list[list[str]]
1130
+ ) -> None:
1131
+ """The content validation spawns nothing and never drops `--no-deps`: the verdict cannot
1132
+ depend on the machine's registry cache or network."""
1133
+ stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock(drop=("serde",)))
1134
+ assert [argv[:2] for argv in cargo_accepts] == [["cargo", "--version"], ["cargo", "metadata"]]
1135
+ metadata = cargo_accepts[1]
1136
+ for flag in ("--locked", "--offline", "--no-deps"):
1137
+ assert flag in metadata
1138
+ assert metadata[metadata.index("--manifest-path") + 1] == str(tmp_path / "backend" / "Cargo.toml")
1139
+
1140
+
1141
+ def test_without_cargo_the_gate_is_still_could_not_run(
1142
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1143
+ ) -> None:
1144
+ """The cargo guard is kept: a stale lock does not turn into a verdict on a machine without cargo."""
1145
+ monkeypatch.setattr(module.shutil, "which", lambda name: None)
1146
+ verdict = stale_lock(tmp_path, SINGLE.files, "version = 99\n")
1147
+ assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
1148
+ assert "cargo is required" in verdict.message
1149
+
1150
+
1151
+ def test_the_missing_lockfile_is_still_lck_001_and_returns_first(
1152
+ tmp_path: Path, cargo_accepts: list[list[str]]
1153
+ ) -> None:
1154
+ lay(tmp_path, SINGLE.files, "version = 99\n")
1155
+ (tmp_path / "backend" / "Cargo.lock").unlink()
1156
+ verdict = grade(tmp_path)
1157
+ [finding] = verdict.findings
1158
+ assert (finding.rule, finding.message) == ("LCK-001", "missing backend/Cargo.lock")
1159
+ assert cargo_accepts == []
1160
+
1161
+
1162
+ # ------------------------------------------------------------------- the documented limit
1163
+
1164
+
1165
+ def test_version_requirement_drift_is_not_detected_offline(
1166
+ tmp_path: Path, cargo_accepts: list[list[str]]
1167
+ ) -> None:
1168
+ """A documented LIMIT, pinned so nobody mistakes it for coverage: the manifest asks for
1169
+ `serde = "99"` and the lock holds `serde 1.0.0`. Presence, structure and membership are
1170
+ checked offline; whether a locked VERSION still satisfies a manifest REQUIREMENT is
1171
+ `cargo metadata` without `--no-deps`, which needs the registry. When that changes on
1172
+ purpose, this test changes with it."""
1173
+ manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "99"\n'
1174
+ verdict = stale_lock(
1175
+ tmp_path,
1176
+ {"Cargo.toml": manifest},
1177
+ lock_text((Pkg("app", "0.1.0", path=True, needs=("serde",)), Pkg("serde", "1.0.0"))),
1178
+ )
1179
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()