boost-skill-cli 1.0.92__tar.gz → 1.0.93__tar.gz

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 (239) hide show
  1. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/PKG-INFO +1 -1
  2. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/_version.py +2 -2
  3. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/pkg.py +20 -1
  4. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/logs.py +5 -0
  5. boost_skill_cli-1.0.93/boost_cli/core/typosquat.py +85 -0
  6. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  7. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/SOURCES.txt +3 -0
  8. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/log-timestamps-mislabeled-utc.md +3 -3
  9. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/typosquat-and-name-confusion-detection.md +3 -3
  10. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap.html +2 -2
  11. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_logs.py +36 -0
  12. boost_skill_cli-1.0.93/tests/unit/test_pkg_confusions.py +45 -0
  13. boost_skill_cli-1.0.93/tests/unit/test_typosquat.py +132 -0
  14. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/.gitattributes +0 -0
  15. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/.gitignore +0 -0
  16. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/CLAUDE.md +0 -0
  17. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/CONTRIBUTING.md +0 -0
  18. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/LICENSE +0 -0
  19. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/MANIFEST.in +0 -0
  20. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/Makefile +0 -0
  21. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/README.md +0 -0
  22. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/SECURITY.md +0 -0
  23. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost +0 -0
  24. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/__init__.py +0 -0
  25. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/__main__.py +0 -0
  26. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/cli.py +0 -0
  27. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/cliparse.py +0 -0
  28. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/__init__.py +0 -0
  29. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/bmad.py +0 -0
  30. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/configuration.py +0 -0
  31. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/discovery.py +0 -0
  32. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/hooks.py +0 -0
  33. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/info.py +0 -0
  34. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/intelligence.py +0 -0
  35. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/quality.py +0 -0
  36. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/taps.py +0 -0
  37. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/team.py +0 -0
  38. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/__init__.py +0 -0
  39. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/agents.py +0 -0
  40. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/ai.py +0 -0
  41. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/catalog.py +0 -0
  42. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/claude_settings.py +0 -0
  43. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/config.py +0 -0
  44. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/dense.py +0 -0
  45. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/embed.py +0 -0
  46. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/frontmatter.py +0 -0
  47. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/gitutil.py +0 -0
  48. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/journal.py +0 -0
  49. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/lockfile.py +0 -0
  50. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/mcp.py +0 -0
  51. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/output.py +0 -0
  52. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/paths.py +0 -0
  53. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/policy.py +0 -0
  54. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/rag.py +0 -0
  55. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/registry.py +0 -0
  56. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/staleness.py +0 -0
  57. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/store.py +0 -0
  58. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/util.py +0 -0
  59. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/data/registries.json +0 -0
  60. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/errors.py +0 -0
  61. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/spin.py +0 -0
  62. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  63. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  64. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/requires.txt +0 -0
  65. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/top_level.txt +0 -0
  66. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/.claude/settings.local.json +0 -0
  67. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/DEBUGGING.md +0 -0
  68. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/demo.gif +0 -0
  69. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/demo.tape +0 -0
  70. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/design-roadmap.html +0 -0
  71. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/index.html +0 -0
  72. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/mcp-hub.html +0 -0
  73. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/rag-architecture.md +0 -0
  74. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D01.md +0 -0
  75. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D02.md +0 -0
  76. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D03.md +0 -0
  77. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D04.md +0 -0
  78. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D05.md +0 -0
  79. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D06.md +0 -0
  80. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D07.md +0 -0
  81. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D08.md +0 -0
  82. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D09.md +0 -0
  83. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D10.md +0 -0
  84. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D11.md +0 -0
  85. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D12.md +0 -0
  86. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D13.md +0 -0
  87. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D14.md +0 -0
  88. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D15.md +0 -0
  89. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D16.md +0 -0
  90. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D17.md +0 -0
  91. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D18.md +0 -0
  92. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D19.md +0 -0
  93. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D20.md +0 -0
  94. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D21.md +0 -0
  95. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D22.md +0 -0
  96. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D23.md +0 -0
  97. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D24.md +0 -0
  98. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D25.md +0 -0
  99. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D26.md +0 -0
  100. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/accessibility-audit-pa11y-ci-axe-core.md +0 -0
  101. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/atomic-corruption-safe-lock-file-writes.md +0 -0
  102. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/atomic-skill-install-temp-dir-swap.md +0 -0
  103. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/autonomous-ship-workflow-and-isolated-worktree.md +0 -0
  104. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/bring-commands-under-mutation-testing.md +0 -0
  105. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/broken-link-and-anchor-checking-lychee.md +0 -0
  106. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/build-provenance-slsa-attestations.md +0 -0
  107. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/cache-the-catalog-entry-set-across-rag-queries.md +0 -0
  108. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/capability-manifest-and-least-privilege-policy.md +0 -0
  109. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/clean-env-install-smoke-pip-and-pipx.md +0 -0
  110. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/command-reference-docs-site.md +0 -0
  111. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/complexity-and-dead-code-radar-xenon-vulture.md +0 -0
  112. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/consolidate-skill-staleness-drift-logic-into-cor.md +0 -0
  113. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/coverage-dashboard-codecov-free-for-oss.md +0 -0
  114. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/coverage-guided-fuzzing-atheris-oss-fuzz.md +0 -0
  115. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/crash-recorder-error-paths-core-logs-py.md +0 -0
  116. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/dependency-cve-gate-pip-audit.md +0 -0
  117. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/docstring-coverage-interrogate.md +0 -0
  118. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/end-of-options-guard-on-git-commands.md +0 -0
  119. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/extension-free-tests-core-dense-py.md +0 -0
  120. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +0 -0
  121. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/finish-mutation-hardening-across-core.md +0 -0
  122. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/fix-self-update-version-detection-dead-branch.md +0 -0
  123. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/fork-safe-network-proxy-handler.md +0 -0
  124. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/frontmatter-scalar-over-coercion.md +0 -0
  125. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/github-pages-deploy-is-broken-every-push.md +0 -0
  126. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/hash-pinned-reproducible-toolchain-uv-lock.md +0 -0
  127. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/html-validation-html-validate.md +0 -0
  128. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/integrity-verification-boost-verify.md +0 -0
  129. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/invocation-pid-logging-for-crash-correlation.md +0 -0
  130. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/layering-guard-import-linter.md +0 -0
  131. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/lighthouse-ci-on-the-pages-site.md +0 -0
  132. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/localize-the-stored-bm25-snippet.md +0 -0
  133. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/lockfile-enforcement-and-commit-pinning.md +0 -0
  134. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/lowest-version-resolution-uv-resolution-lowest-d.md +0 -0
  135. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/make-the-roadmaps-discoverable.md +0 -0
  136. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/markdown-consistency-markdownlint-cli2.md +0 -0
  137. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mcp-launch-objc-fork-safety.md +0 -0
  138. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/memoize-config-load-in-process.md +0 -0
  139. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/modernization-smells-refurb-pyupgrade.md +0 -0
  140. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mutation-hardening-core-frontmatter-py.md +0 -0
  141. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mutation-hardening-core-gitutil-py.md +0 -0
  142. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mutation-hardening-core-store-py.md +0 -0
  143. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/one-command-every-env-nox.md +0 -0
  144. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/one-shared-atomic-write-helper.md +0 -0
  145. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/package-metadata-validation-twine-check-friends.md +0 -0
  146. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/patch-coverage-gate-diff-cover.md +0 -0
  147. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/performance-regression-gate-pytest-benchmark.md +0 -0
  148. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/post-deploy-smoke-headless-load-check.md +0 -0
  149. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/pre-release-python-canary-3-14t-free-threaded.md +0 -0
  150. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/promote-nav-footer-into-the-shared-style-system.md +0 -0
  151. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/prompt-injection-scanning-of-skill-markdown.md +0 -0
  152. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/property-based-tests-hypothesis-on-the-parsers.md +0 -0
  153. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/prose-and-terminology-linting-vale.md +0 -0
  154. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/prune-ignored-dirs-during-scan-dir-walk.md +0 -0
  155. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/quality-dashboard-sonarcloud-free-for-oss.md +0 -0
  156. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/reconcile-the-theme-drift.md +0 -0
  157. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/refresh-the-marketing-surface.md +0 -0
  158. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/reuse-helpers-kill-minor-dead-work.md +0 -0
  159. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/robust-tag-argument-parsing.md +0 -0
  160. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/rule-install-native-materialization.md +0 -0
  161. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/runner-egress-monitoring-stepsecurity-harden-run.md +0 -0
  162. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/sbom-aware-scanning-osv-scanner.md +0 -0
  163. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/sbom-on-every-release-cyclonedx-syft.md +0 -0
  164. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/second-type-checker-pyright.md +0 -0
  165. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/secret-and-pii-scanning-of-installed-skills.md +0 -0
  166. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/secret-scanning-gitleaks-push-protection.md +0 -0
  167. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/security-linting-bandit-via-ruff-s-rules.md +0 -0
  168. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/shift-left-gate-pre-commit-pre-commit-ci.md +0 -0
  169. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/single-imperative-rule-extractor.md +0 -0
  170. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/single-tech-stack-prober.md +0 -0
  171. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/split-oversized-command-modules.md +0 -0
  172. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/startup-and-import-time-budget-x-importtime.md +0 -0
  173. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/stop-re-serializing-entry-meta-on-every-search.md +0 -0
  174. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/supply-chain-posture-openssf-scorecard.md +0 -0
  175. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/tap-signing-and-provenance-sigstore-minisign.md +0 -0
  176. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/theme-asset-linting-stylelint-eslint.md +0 -0
  177. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/typo-detection-codespell.md +0 -0
  178. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/unify-tilde-two-copies-have-a-boundary-bug.md +0 -0
  179. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/untrack-generated-build-noise.md +0 -0
  180. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/update-diff-before-apply.md +0 -0
  181. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/visual-regression-pass-on-the-guide.md +0 -0
  182. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/widen-the-ruff-rule-surface-b-sim-c4-perf-ruf.md +0 -0
  183. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/windows-in-the-ci-matrix.md +0 -0
  184. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/workflow-linting-actionlint.md +0 -0
  185. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/workflow-sast-zizmor.md +0 -0
  186. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/workspace-scope-install.md +0 -0
  187. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/index.html +0 -0
  188. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/pyproject.toml +0 -0
  189. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/scripts/build_registries.py +0 -0
  190. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/scripts/build_roadmap.py +0 -0
  191. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/scripts/mutation_gate.py +0 -0
  192. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/setup.cfg +0 -0
  193. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/README.md +0 -0
  194. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/boost.css +0 -0
  195. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/boost.js +0 -0
  196. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/demo.html +0 -0
  197. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/conftest.py +0 -0
  198. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_bmad.py +0 -0
  199. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_configuration.py +0 -0
  200. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_discovery.py +0 -0
  201. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_hooks.py +0 -0
  202. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_info.py +0 -0
  203. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_intelligence.py +0 -0
  204. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_pkg.py +0 -0
  205. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_quality.py +0 -0
  206. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_taps.py +0 -0
  207. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_team.py +0 -0
  208. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_everyday_loop.py +0 -0
  209. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/make_fixture.py +0 -0
  210. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/smoke.sh +0 -0
  211. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_agents.py +0 -0
  212. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_ai.py +0 -0
  213. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_catalog.py +0 -0
  214. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_claude_settings.py +0 -0
  215. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_cliparse.py +0 -0
  216. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_config.py +0 -0
  217. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_dense.py +0 -0
  218. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_dense_fallback.py +0 -0
  219. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_embed.py +0 -0
  220. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_errors_and_cli_table.py +0 -0
  221. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_frontmatter.py +0 -0
  222. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_gitutil.py +0 -0
  223. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_journal.py +0 -0
  224. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_lockfile.py +0 -0
  225. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_mcp.py +0 -0
  226. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_mutation_hardening.py +0 -0
  227. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_output.py +0 -0
  228. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_paths.py +0 -0
  229. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_policy.py +0 -0
  230. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_rag.py +0 -0
  231. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_registries_fresh.py +0 -0
  232. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_registry.py +0 -0
  233. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_roadmap_fresh.py +0 -0
  234. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_spin.py +0 -0
  235. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_staleness.py +0 -0
  236. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_store.py +0 -0
  237. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_token_parity.py +0 -0
  238. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_util.py +0 -0
  239. {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.92
3
+ Version: 1.0.93
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.92'
22
- __version_tuple__ = version_tuple = (1, 0, 92)
21
+ __version__ = version = '1.0.93'
22
+ __version_tuple__ = version_tuple = (1, 0, 93)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -16,7 +16,7 @@ from typing import Dict, List, Optional
16
16
 
17
17
  from .. import cliparse
18
18
  from ..core import (agents, catalog, gitutil, journal, lockfile, paths,
19
- registry, staleness, store, util)
19
+ registry, staleness, store, typosquat, util)
20
20
  from ..core import output as out
21
21
  from ..errors import BoostError
22
22
 
@@ -35,6 +35,23 @@ def _plural(n: int, word: str) -> str:
35
35
  return "%d %s%s" % (n, word, "" if n == 1 else "s")
36
36
 
37
37
 
38
+ def _warn_confusions(entries: List[dict]) -> None:
39
+ """Warn when a skill about to be installed is confusable with one from a
40
+ *different* tap — the typosquat / owner-confusion guard (core.typosquat).
41
+
42
+ Quiet by default: it only speaks up when a near-identical name resolves to
43
+ another owner, which is exactly the fat-finger-into-a-look-alike case.
44
+ """
45
+ if not entries:
46
+ return
47
+ all_entries = catalog.all_entries()
48
+ for e in entries:
49
+ for other in typosquat.find_confusions(e, all_entries)[:3]:
50
+ out.warn("%s (%s) closely resembles %s (%s) — double-check you "
51
+ "meant this one" % (e["name"], e["tap"],
52
+ other["name"], other["tap"]))
53
+
54
+
38
55
  def _check_agents(names: Optional[List[str]]) -> Optional[List[str]]:
39
56
  """Validate --agent values against configured agents."""
40
57
  if not names:
@@ -106,6 +123,8 @@ def cmd_install(argv: List[str]) -> int:
106
123
  out.warn("%s: %s" % (n, err.message))
107
124
  failed += 1
108
125
 
126
+ _warn_confusions(entries)
127
+
109
128
  if args.dry_run:
110
129
  targets = [a for a in agents.enabled_agents() if not only or a in only]
111
130
  for e in entries:
@@ -36,6 +36,7 @@ import logging.handlers
36
36
  import os
37
37
  import platform
38
38
  import sys
39
+ import time
39
40
  import traceback
40
41
  from datetime import datetime, timezone
41
42
  from pathlib import Path
@@ -143,6 +144,10 @@ def configure(verbose: bool = False, debug: bool = False,
143
144
  "%(asctime)s %(levelname)-7s %(name)s: %(message)s",
144
145
  datefmt="%Y-%m-%dT%H:%M:%SZ",
145
146
  )
147
+ # datefmt stamps a literal ``Z`` (UTC) suffix, so the timestamp must be
148
+ # rendered in UTC — otherwise ``%(asctime)s`` uses local time and every
149
+ # line is mislabelled by the machine's offset. gmtime keeps it honest.
150
+ fmt.converter = time.gmtime
146
151
 
147
152
  # File handler — always DEBUG, best-effort (never break the CLI over a log).
148
153
  if _file_enabled():
@@ -0,0 +1,85 @@
1
+ """Detect typosquatted / confusable skill names (stdlib only).
2
+
3
+ The classic package-manager attack is a skill named one keystroke away from a
4
+ popular one, or a familiar name that quietly resolves to a *different*
5
+ owner/repo. These helpers measure how close two names are and flag catalog
6
+ entries that are suspiciously similar to — but not the same source as — the one
7
+ a user asked for, so a command can warn before installing a look-alike.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import List
12
+
13
+
14
+ def edit_distance(a: str, b: str, cap: int = 2) -> int:
15
+ """Levenshtein distance between ``a`` and ``b``, not measured past ``cap``.
16
+
17
+ Returns the true distance when it is ``<= cap``, otherwise ``cap + 1`` — a
18
+ sentinel meaning "farther apart than we care about". The row-minimum
19
+ early-exit keeps a comparison against a large catalog cheap: as soon as an
20
+ entire DP row exceeds ``cap`` no completion can come back under it.
21
+ """
22
+ if a == b:
23
+ return 0
24
+ la, lb = len(a), len(b)
25
+ if abs(la - lb) > cap: # a length gap alone already exceeds cap
26
+ return cap + 1
27
+ if la > lb: # iterate over the shorter string's rows
28
+ a, b, la, lb = b, a, lb, la
29
+ prev = list(range(lb + 1))
30
+ for i in range(1, la + 1):
31
+ cur = [i] + [0] * lb
32
+ row_min = i
33
+ ca = a[i - 1]
34
+ for j in range(1, lb + 1):
35
+ cost = 0 if ca == b[j - 1] else 1
36
+ cur[j] = min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)
37
+ if cur[j] < row_min:
38
+ row_min = cur[j]
39
+ if row_min > cap:
40
+ return cap + 1
41
+ prev = cur
42
+ return prev[lb] if prev[lb] <= cap else cap + 1
43
+
44
+
45
+ def confusable_names(name: str, candidates, max_distance: int = 1) -> List[str]:
46
+ """Candidate names within ``1..max_distance`` edits of ``name``.
47
+
48
+ Case-insensitive; exact matches (distance 0) and duplicates are dropped.
49
+ Ordered by ascending distance, then name, so the closest look-alike is
50
+ first.
51
+ """
52
+ target = name.lower()
53
+ hits = []
54
+ seen = set()
55
+ for cand in candidates:
56
+ low = cand.lower()
57
+ if low == target or low in seen:
58
+ continue
59
+ d = edit_distance(target, low, max_distance)
60
+ if 1 <= d <= max_distance:
61
+ hits.append((d, cand))
62
+ seen.add(low)
63
+ hits.sort(key=lambda t: (t[0], t[1]))
64
+ return [c for _d, c in hits]
65
+
66
+
67
+ def find_confusions(target: dict, entries, max_distance: int = 1) -> List[dict]:
68
+ """Catalog entries that are confusable with ``target`` but from another tap.
69
+
70
+ Flags both the owner-confusion case (same name, *different* tap → distance
71
+ 0) and the typosquat case (name within ``max_distance`` edits, different
72
+ tap). Entries from ``target``'s own tap are never flagged, and ``target``
73
+ itself is excluded by identity. Sorted by ``(distance, name, tap)``.
74
+ """
75
+ tname = str(target.get("name", "")).lower()
76
+ ttap = target.get("tap")
77
+ scored = []
78
+ for e in entries:
79
+ if e is target or e.get("tap") == ttap:
80
+ continue
81
+ d = edit_distance(tname, str(e.get("name", "")).lower(), max_distance)
82
+ if d <= max_distance:
83
+ scored.append((d, e))
84
+ scored.sort(key=lambda t: (t[0], str(t[1].get("name")), str(t[1].get("tap"))))
85
+ return [e for _d, e in scored]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.92
3
+ Version: 1.0.93
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -50,6 +50,7 @@ boost_cli/core/rag.py
50
50
  boost_cli/core/registry.py
51
51
  boost_cli/core/staleness.py
52
52
  boost_cli/core/store.py
53
+ boost_cli/core/typosquat.py
53
54
  boost_cli/core/util.py
54
55
  boost_cli/data/registries.json
55
56
  boost_skill_cli.egg-info/PKG-INFO
@@ -222,6 +223,7 @@ tests/unit/test_mcp.py
222
223
  tests/unit/test_mutation_hardening.py
223
224
  tests/unit/test_output.py
224
225
  tests/unit/test_paths.py
226
+ tests/unit/test_pkg_confusions.py
225
227
  tests/unit/test_policy.py
226
228
  tests/unit/test_rag.py
227
229
  tests/unit/test_registries_fresh.py
@@ -231,5 +233,6 @@ tests/unit/test_spin.py
231
233
  tests/unit/test_staleness.py
232
234
  tests/unit/test_store.py
233
235
  tests/unit/test_token_parity.py
236
+ tests/unit/test_typosquat.py
234
237
  tests/unit/test_util.py
235
238
  tests/unit/test_version.py
@@ -2,15 +2,15 @@
2
2
  id: log-timestamps-mislabeled-utc
3
3
  board: code
4
4
  section: health
5
- status: planned
5
+ status: shipped
6
6
  category: Observability · Diagnostics
7
7
  complexity: S
8
8
  impact: Med
9
9
  wow: 2
10
10
  note: converter = time.gmtime
11
11
  order: 10
12
- owner:
13
- pr:
12
+ owner: loop/utc-log-timestamps
13
+ pr: 104
14
14
  title: Log timestamps are local time mislabeled <code>Z</code>
15
15
  ---
16
16
  <code>core/logs.py</code> builds the <code>logging.Formatter</code> with
@@ -2,15 +2,15 @@
2
2
  id: typosquat-and-name-confusion-detection
3
3
  board: code
4
4
  section: trust
5
- status: planned
5
+ status: shipped
6
6
  category: Security · Registry
7
7
  complexity: M
8
8
  impact: Med
9
9
  wow: 4
10
10
  note: edit-distance guard
11
11
  order: 4
12
- owner:
13
- pr:
12
+ owner: loop/typosquat-detect
13
+ pr: 107
14
14
  title: Typosquat &amp; name-confusion detection
15
15
  ---
16
16
  The classic package-manager attack: a skill named one edit-distance from
@@ -836,7 +836,7 @@
836
836
  </article>
837
837
 
838
838
  <article class="cap rcard">
839
- <div class="head"><span class="pill planned">Planned</span><span class="cat">Observability · Diagnostics</span></div>
839
+ <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Observability · Diagnostics</span></div>
840
840
  <h3>Log timestamps are local time mislabeled <code>Z</code></h3>
841
841
  <p><code>core/logs.py</code> builds the <code>logging.Formatter</code> with
842
842
  <code>datefmt="%Y-%m-%dT%H:%M:%SZ"</code> but never sets
@@ -1560,7 +1560,7 @@ and the work stays visible.</p>
1560
1560
  </article>
1561
1561
 
1562
1562
  <article class="cap rcard">
1563
- <div class="head"><span class="pill planned">Planned</span><span class="cat">Security · Registry</span></div>
1563
+ <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Security · Registry</span></div>
1564
1564
  <h3>Typosquat &amp; name-confusion detection</h3>
1565
1565
  <p>The classic package-manager attack: a skill named one edit-distance from
1566
1566
  a popular one, or a familiar name that quietly resolves to an unexpected
@@ -2,7 +2,9 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import logging
5
+ import logging.handlers
5
6
  import pathlib
7
+ import time
6
8
 
7
9
  import pytest
8
10
 
@@ -342,3 +344,37 @@ def test_doctor_reports_log_location(boost):
342
344
  boost("count")
343
345
  res = boost("doctor", expect=None)
344
346
  assert "diagnostic log at" in res.out
347
+
348
+
349
+ def _file_formatter():
350
+ """The formatter attached to the configured file handler."""
351
+ for h in logs.get_logger().handlers:
352
+ if isinstance(h, logging.handlers.RotatingFileHandler):
353
+ return h.formatter
354
+ raise AssertionError("no file handler configured")
355
+
356
+
357
+ class TestUtcTimestamps:
358
+ def test_formatter_uses_gmtime_converter(self, env):
359
+ # datefmt stamps a literal ``Z`` (UTC) suffix, so the converter MUST be
360
+ # gmtime; the default (localtime) would mislabel every line.
361
+ logs.configure()
362
+ assert _file_formatter().converter is time.gmtime
363
+
364
+ def test_asctime_is_utc_regardless_of_local_zone(self, env, monkeypatch):
365
+ # Force a non-UTC local zone; the rendered stamp must still be UTC.
366
+ if not hasattr(time, "tzset"):
367
+ pytest.skip("time.tzset unavailable on this platform")
368
+ monkeypatch.setenv("TZ", "America/New_York") # UTC-5/-4, never UTC
369
+ time.tzset()
370
+ try:
371
+ logs.configure()
372
+ fmt = _file_formatter()
373
+ record = logging.LogRecord(
374
+ "boost", logging.INFO, __file__, 1, "msg", None, None
375
+ )
376
+ record.created = 0.0 # 1970-01-01T00:00:00Z
377
+ assert fmt.formatTime(record, fmt.datefmt) == "1970-01-01T00:00:00Z"
378
+ finally:
379
+ monkeypatch.delenv("TZ", raising=False)
380
+ time.tzset()
@@ -0,0 +1,45 @@
1
+ """Unit tests: pkg._warn_confusions — the install-time typosquat guard wiring."""
2
+ from __future__ import annotations
3
+
4
+ from boost_cli.commands import pkg
5
+ from boost_cli.core import catalog
6
+ from boost_cli.core import output as out
7
+
8
+
9
+ def _capture(monkeypatch, entries):
10
+ monkeypatch.setattr(catalog, "all_entries", lambda: entries)
11
+ warnings = []
12
+ monkeypatch.setattr(out, "warn", lambda m: warnings.append(m))
13
+ return warnings
14
+
15
+
16
+ def test_warns_on_cross_tap_lookalike(monkeypatch):
17
+ entries = [{"name": "deploy", "tap": "alice/skills"},
18
+ {"name": "deployy", "tap": "mallory/evil"}]
19
+ warnings = _capture(monkeypatch, entries)
20
+ pkg._warn_confusions([entries[0]])
21
+ assert len(warnings) == 1
22
+ assert "mallory/evil" in warnings[0] and "deploy" in warnings[0]
23
+
24
+
25
+ def test_quiet_when_no_confusion(monkeypatch):
26
+ entries = [{"name": "deploy", "tap": "alice/skills"},
27
+ {"name": "totally-unrelated", "tap": "bob/x"}]
28
+ warnings = _capture(monkeypatch, entries)
29
+ pkg._warn_confusions([entries[0]])
30
+ assert warnings == []
31
+
32
+
33
+ def test_same_tap_sibling_not_warned(monkeypatch):
34
+ entries = [{"name": "deploy", "tap": "alice/skills"},
35
+ {"name": "deployy", "tap": "alice/skills"}]
36
+ warnings = _capture(monkeypatch, entries)
37
+ pkg._warn_confusions([entries[0]])
38
+ assert warnings == []
39
+
40
+
41
+ def test_empty_entries_skips_catalog_lookup(monkeypatch):
42
+ def boom():
43
+ raise AssertionError("catalog must not be queried for an empty install")
44
+ monkeypatch.setattr(catalog, "all_entries", boom)
45
+ pkg._warn_confusions([]) # returns before touching the catalog
@@ -0,0 +1,132 @@
1
+ """Unit tests: boost_cli/core/typosquat.py — edit distance & name confusion."""
2
+ from __future__ import annotations
3
+
4
+ from boost_cli.core import typosquat
5
+
6
+
7
+ class TestEditDistance:
8
+ def test_identical_is_zero(self):
9
+ assert typosquat.edit_distance("brainstorm", "brainstorm") == 0
10
+
11
+ def test_single_substitution(self):
12
+ assert typosquat.edit_distance("cat", "cot") == 1
13
+
14
+ def test_single_insertion(self):
15
+ assert typosquat.edit_distance("cat", "cart") == 1
16
+
17
+ def test_single_deletion(self):
18
+ assert typosquat.edit_distance("cart", "cat") == 1
19
+
20
+ def test_transposition_costs_two(self):
21
+ # Levenshtein (no adjacent-swap rule) scores a swap as two edits
22
+ assert typosquat.edit_distance("ab", "ba") == 2
23
+
24
+ def test_symmetric(self):
25
+ assert (typosquat.edit_distance("kitten", "sitting", cap=5)
26
+ == typosquat.edit_distance("sitting", "kitten", cap=5))
27
+
28
+ def test_classic_kitten_sitting_is_three(self):
29
+ assert typosquat.edit_distance("kitten", "sitting", cap=9) == 3
30
+
31
+ def test_empty_strings(self):
32
+ assert typosquat.edit_distance("", "") == 0
33
+ assert typosquat.edit_distance("", "abc", cap=5) == 3
34
+
35
+ def test_cap_returns_sentinel_when_farther(self):
36
+ # true distance is 3; with cap=1 we only learn "> cap" as cap + 1
37
+ assert typosquat.edit_distance("abcde", "xyzde", cap=1) == 2
38
+
39
+ def test_length_gap_shortcut(self):
40
+ # length difference alone exceeds the cap -> sentinel, no DP needed
41
+ assert typosquat.edit_distance("a", "aaaaaa", cap=2) == 3
42
+
43
+ def test_true_distance_returned_within_cap(self):
44
+ assert typosquat.edit_distance("foo", "foobar", cap=5) == 3
45
+
46
+
47
+ class TestConfusableNames:
48
+ NAMES = ["brainstorm", "brainstorms", "brainstom", "commit", "unrelated"]
49
+
50
+ def test_finds_distance_one_only_by_default(self):
51
+ got = typosquat.confusable_names("brainstorm", self.NAMES)
52
+ assert got == ["brainstom", "brainstorms"] # both 1 edit away
53
+
54
+ def test_exact_match_excluded(self):
55
+ assert "brainstorm" not in typosquat.confusable_names(
56
+ "brainstorm", self.NAMES)
57
+
58
+ def test_distance_two_needs_higher_max(self):
59
+ # "brainstrom" is 2 edits from "brainstorm" (swap) -> hidden at max 1
60
+ cands = ["brainstrom", "brainstorm"]
61
+ assert typosquat.confusable_names("brainstorm", cands) == []
62
+ assert typosquat.confusable_names(
63
+ "brainstorm", cands, max_distance=2) == ["brainstrom"]
64
+
65
+ def test_case_insensitive(self):
66
+ assert typosquat.confusable_names("Commit", ["commiy"]) == ["commiy"]
67
+
68
+ def test_deduplicates_case_variants(self):
69
+ assert typosquat.confusable_names("cat", ["cot", "COT"]) == ["cot"]
70
+
71
+ def test_sorted_by_distance_then_name(self):
72
+ # "care" is 2 from "cat"; "bat"/"cot" are 1 -> closest first, then a-z
73
+ got = typosquat.confusable_names("cat", ["cot", "bat", "care"],
74
+ max_distance=2)
75
+ assert got == ["bat", "cot", "care"]
76
+
77
+ def test_empty_candidates(self):
78
+ assert typosquat.confusable_names("cat", []) == []
79
+
80
+
81
+ def _e(name, tap):
82
+ return {"name": name, "tap": tap}
83
+
84
+
85
+ class TestFindConfusions:
86
+ def test_same_name_different_tap_flagged(self):
87
+ target = _e("deploy", "alice/skills")
88
+ entries = [target, _e("deploy", "mallory/evil")]
89
+ got = typosquat.find_confusions(target, entries)
90
+ assert [x["tap"] for x in got] == ["mallory/evil"]
91
+
92
+ def test_near_name_different_tap_flagged(self):
93
+ target = _e("deploy", "alice/skills")
94
+ got = typosquat.find_confusions(
95
+ target, [target, _e("deployy", "mallory/evil")])
96
+ assert [x["name"] for x in got] == ["deployy"]
97
+
98
+ def test_same_tap_never_flagged(self):
99
+ target = _e("deploy", "alice/skills")
100
+ # a sibling one edit away in the SAME tap is not a confusion
101
+ got = typosquat.find_confusions(
102
+ target, [target, _e("deployy", "alice/skills")])
103
+ assert got == []
104
+
105
+ def test_target_identity_excluded(self):
106
+ target = _e("deploy", "alice/skills")
107
+ assert typosquat.find_confusions(target, [target]) == []
108
+
109
+ def test_far_name_not_flagged(self):
110
+ target = _e("deploy", "alice/skills")
111
+ got = typosquat.find_confusions(
112
+ target, [target, _e("something-else", "mallory/evil")])
113
+ assert got == []
114
+
115
+ def test_respects_max_distance(self):
116
+ target = _e("deploy", "alice/skills")
117
+ entries = [target, _e("deproy", "x/y")] # 1 edit
118
+ assert len(typosquat.find_confusions(target, entries)) == 1
119
+ assert typosquat.find_confusions(
120
+ target, [target, _e("depployy", "x/y")]) == [] # 2 edits, max 1
121
+
122
+ def test_sorted_by_distance_then_name_then_tap(self):
123
+ target = _e("deploy", "alice/skills")
124
+ entries = [
125
+ target,
126
+ _e("deployy", "z/last"), # dist 1
127
+ _e("deploy", "a/first"), # dist 0 -> should come first
128
+ _e("deploy", "m/mid"), # dist 0
129
+ ]
130
+ got = typosquat.find_confusions(target, entries)
131
+ assert [(x["name"], x["tap"]) for x in got] == [
132
+ ("deploy", "a/first"), ("deploy", "m/mid"), ("deployy", "z/last")]
File without changes