mdfetch 0.5.0__tar.gz → 0.5.2__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 (209) hide show
  1. mdfetch-0.5.2/.github/workflows/publish.yml +120 -0
  2. mdfetch-0.5.2/.specify/feature.json +3 -0
  3. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/changelog.md +20 -0
  4. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/plan.md +39 -1
  5. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/spec.md +83 -1
  6. {mdfetch-0.5.0 → mdfetch-0.5.2}/CLAUDE.md +5 -4
  7. {mdfetch-0.5.0 → mdfetch-0.5.2}/PKG-INFO +11 -1
  8. {mdfetch-0.5.0 → mdfetch-0.5.2}/README.md +10 -0
  9. {mdfetch-0.5.0 → mdfetch-0.5.2}/pyproject.toml +1 -1
  10. mdfetch-0.5.2/specs/009-homebrew-tap-formula/checklists/requirements.md +36 -0
  11. mdfetch-0.5.2/specs/009-homebrew-tap-formula/contracts/formula.md +119 -0
  12. mdfetch-0.5.2/specs/009-homebrew-tap-formula/contracts/tap-update-job.md +86 -0
  13. mdfetch-0.5.2/specs/009-homebrew-tap-formula/data-model.md +66 -0
  14. mdfetch-0.5.2/specs/009-homebrew-tap-formula/plan.md +152 -0
  15. mdfetch-0.5.2/specs/009-homebrew-tap-formula/quickstart.md +76 -0
  16. mdfetch-0.5.2/specs/009-homebrew-tap-formula/research.md +116 -0
  17. mdfetch-0.5.2/specs/009-homebrew-tap-formula/spec.md +121 -0
  18. mdfetch-0.5.2/specs/009-homebrew-tap-formula/tasks.md +172 -0
  19. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/base.py +33 -10
  20. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/cli.py +23 -3
  21. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/devto.py +9 -9
  22. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/medium.py +11 -3
  23. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/substack.py +10 -10
  24. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/router.py +7 -6
  25. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_cli.py +25 -0
  26. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_medium_extractor.py +23 -6
  27. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_router.py +20 -0
  28. {mdfetch-0.5.0 → mdfetch-0.5.2}/uv.lock +1 -1
  29. mdfetch-0.5.0/.github/workflows/publish.yml +0 -55
  30. mdfetch-0.5.0/.specify/feature.json +0 -3
  31. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-analyze/SKILL.md +0 -0
  32. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-archive-run/SKILL.md +0 -0
  33. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-checklist/SKILL.md +0 -0
  34. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-clarify/SKILL.md +0 -0
  35. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-constitution/SKILL.md +0 -0
  36. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
  37. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
  38. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
  39. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
  40. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
  41. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-implement/SKILL.md +0 -0
  42. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-plan/SKILL.md +0 -0
  43. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-reconcile-run/SKILL.md +0 -0
  44. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-specify/SKILL.md +0 -0
  45. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-tasks/SKILL.md +0 -0
  46. {mdfetch-0.5.0 → mdfetch-0.5.2}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
  47. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.analyze.toml +0 -0
  48. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.archive.run.toml +0 -0
  49. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.checklist.toml +0 -0
  50. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.clarify.toml +0 -0
  51. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.constitution.toml +0 -0
  52. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.implement.toml +0 -0
  53. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.plan.toml +0 -0
  54. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.reconcile.run.toml +0 -0
  55. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.specify.toml +0 -0
  56. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.tasks.toml +0 -0
  57. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gemini/commands/speckit.taskstoissues.toml +0 -0
  58. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gitattributes +0 -0
  59. {mdfetch-0.5.0 → mdfetch-0.5.2}/.github/copilot-instructions.md +0 -0
  60. {mdfetch-0.5.0 → mdfetch-0.5.2}/.github/workflows/ci.yml +0 -0
  61. {mdfetch-0.5.0 → mdfetch-0.5.2}/.github/workflows/integration.yml +0 -0
  62. {mdfetch-0.5.0 → mdfetch-0.5.2}/.gitignore +0 -0
  63. {mdfetch-0.5.0 → mdfetch-0.5.2}/.python-version +0 -0
  64. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/.registry +0 -0
  65. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/LICENSE +0 -0
  66. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/README.md +0 -0
  67. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/commands/archive.md +0 -0
  68. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/archive/extension.yml +0 -0
  69. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/README.md +0 -0
  70. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
  71. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
  72. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
  73. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
  74. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
  75. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/config-template.yml +0 -0
  76. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/extension.yml +0 -0
  77. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/git-config.yml +0 -0
  78. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
  79. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
  80. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
  81. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
  82. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
  83. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
  84. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
  85. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
  86. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/LICENSE +0 -0
  87. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/README.md +0 -0
  88. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/commands/reconcile.md +0 -0
  89. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions/reconcile/extension.yml +0 -0
  90. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/extensions.yml +0 -0
  91. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/init-options.json +0 -0
  92. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integration.json +0 -0
  93. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integrations/claude.manifest.json +0 -0
  94. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integrations/gemini.manifest.json +0 -0
  95. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/integrations/speckit.manifest.json +0 -0
  96. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/memory/constitution.md +0 -0
  97. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  98. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/common.sh +0 -0
  99. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/create-new-feature.sh +0 -0
  100. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/setup-plan.sh +0 -0
  101. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/scripts/bash/setup-tasks.sh +0 -0
  102. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/checklist-template.md +0 -0
  103. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/constitution-template.md +0 -0
  104. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/plan-template.md +0 -0
  105. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/spec-template.md +0 -0
  106. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/templates/tasks-template.md +0 -0
  107. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/workflows/speckit/workflow.yml +0 -0
  108. {mdfetch-0.5.0 → mdfetch-0.5.2}/.specify/workflows/workflow-registry.json +0 -0
  109. {mdfetch-0.5.0 → mdfetch-0.5.2}/.vscode/settings.json +0 -0
  110. {mdfetch-0.5.0 → mdfetch-0.5.2}/GEMINI.md +0 -0
  111. {mdfetch-0.5.0 → mdfetch-0.5.2}/LICENSE +0 -0
  112. {mdfetch-0.5.0 → mdfetch-0.5.2}/Makefile +0 -0
  113. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/checklists/requirements.md +0 -0
  114. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/contracts/api.md +0 -0
  115. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/data-model.md +0 -0
  116. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/plan.md +0 -0
  117. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/quickstart.md +0 -0
  118. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/research.md +0 -0
  119. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/spec.md +0 -0
  120. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/001-mdfetch-medium-extractor/tasks.md +0 -0
  121. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/checklists/requirements.md +0 -0
  122. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/contracts/public-api.md +0 -0
  123. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/data-model.md +0 -0
  124. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/plan.md +0 -0
  125. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/quickstart.md +0 -0
  126. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/research.md +0 -0
  127. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/spec.md +0 -0
  128. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/002-devto-provider/tasks.md +0 -0
  129. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/checklists/requirements.md +0 -0
  130. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/contracts/extract-api.md +0 -0
  131. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/plan.md +0 -0
  132. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/research.md +0 -0
  133. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/spec.md +0 -0
  134. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/003-medium-freedium-fallback/tasks.md +0 -0
  135. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/checklists/requirements.md +0 -0
  136. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/plan.md +0 -0
  137. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/research.md +0 -0
  138. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/spec.md +0 -0
  139. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/004-remove-backoff/tasks.md +0 -0
  140. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/checklists/requirements.md +0 -0
  141. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/contracts/extractor-api.md +0 -0
  142. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/data-model.md +0 -0
  143. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/plan.md +0 -0
  144. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/quickstart.md +0 -0
  145. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/research.md +0 -0
  146. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/spec.md +0 -0
  147. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/005-substack-provider/tasks.md +0 -0
  148. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/checklists/requirements.md +0 -0
  149. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/contracts/public-api.md +0 -0
  150. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/data-model.md +0 -0
  151. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/plan.md +0 -0
  152. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/quickstart.md +0 -0
  153. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/research.md +0 -0
  154. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/spec.md +0 -0
  155. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/006-thenewstack-provider/tasks.md +0 -0
  156. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/checklists/requirements.md +0 -0
  157. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/contracts/public-api.md +0 -0
  158. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/data-model.md +0 -0
  159. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/plan.md +0 -0
  160. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/quickstart.md +0 -0
  161. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/research.md +0 -0
  162. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/spec.md +0 -0
  163. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/007-dzone-provider/tasks.md +0 -0
  164. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/checklists/requirements.md +0 -0
  165. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/contracts/cli.md +0 -0
  166. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/data-model.md +0 -0
  167. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/plan.md +0 -0
  168. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/quickstart.md +0 -0
  169. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/research.md +0 -0
  170. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/spec.md +0 -0
  171. {mdfetch-0.5.0 → mdfetch-0.5.2}/specs/008-mdfetch-cli/tasks.md +0 -0
  172. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/__init__.py +0 -0
  173. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/exceptions.py +0 -0
  174. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/__init__.py +0 -0
  175. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/dzone.py +0 -0
  176. {mdfetch-0.5.0 → mdfetch-0.5.2}/src/mdfetch/providers/thenewstack.py +0 -0
  177. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/__init__.py +0 -0
  178. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/conftest.py +0 -0
  179. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/__init__.py +0 -0
  180. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/conftest.py +0 -0
  181. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/architecting-the-asynchronous-agent.md +0 -0
  182. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/devto-integration-digest-december-2025.md +0 -0
  183. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/devto-integration-digest-july-2025.md +0 -0
  184. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/devto-integration-digest-march-2026.md +0 -0
  185. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/dzone-image-classification-pipeline-camel-djl.md +0 -0
  186. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/dzone-integration-patterns-fail-production.md +0 -0
  187. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/dzone-kiro-feature-to-requirements-design-tasks.md +0 -0
  188. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/from-drift-to-parity.md +0 -0
  189. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/integration-digest-december-2025.md +0 -0
  190. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/substack-api-trends-2025.md +0 -0
  191. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/substack-kafka-topic-types.md +0 -0
  192. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-api-mcp-agent.md +0 -0
  193. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-async-apis.md +0 -0
  194. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-developer-portal-api.md +0 -0
  195. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-json-schema-ai.md +0 -0
  196. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/snapshots/thenewstack-mcp-api-governance.md +0 -0
  197. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_cli_integration.py +0 -0
  198. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_devto_integration.py +0 -0
  199. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_dzone_integration.py +0 -0
  200. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_medium_integration.py +0 -0
  201. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_substack_integration.py +0 -0
  202. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/integration/test_thenewstack_integration.py +0 -0
  203. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/__init__.py +0 -0
  204. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_devto_extractor.py +0 -0
  205. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_dzone_extractor.py +0 -0
  206. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_fetch_errors.py +0 -0
  207. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_silent.py +0 -0
  208. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_substack_extractor.py +0 -0
  209. {mdfetch-0.5.0 → mdfetch-0.5.2}/tests/unit/test_thenewstack_extractor.py +0 -0
@@ -0,0 +1,120 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ name: Build distribution
13
+ runs-on: ubuntu-latest
14
+ if: github.event.release.target_commitish == 'main'
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v4
19
+ - name: Set up Python
20
+ run: uv python pin 3.12
21
+ - name: Validate version
22
+ run: |
23
+ TAG_VERSION=${GITHUB_REF_NAME#v}
24
+ TOML_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
25
+ if [ "$TAG_VERSION" != "$TOML_VERSION" ]; then
26
+ echo "Error: Tag version ($TAG_VERSION) does not match pyproject.toml version ($TOML_VERSION)"
27
+ exit 1
28
+ fi
29
+ - name: Build package
30
+ run: make build
31
+ - name: Store the distribution packages
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: python-package-distributions
35
+ path: dist/
36
+
37
+ publish:
38
+ name: Publish to PyPI
39
+ runs-on: ubuntu-latest
40
+ needs: build
41
+ environment:
42
+ name: pypi
43
+ url: https://pypi.org/p/mdfetch
44
+ permissions:
45
+ id-token: write
46
+ steps:
47
+ - name: Download all the dists
48
+ uses: actions/download-artifact@v4
49
+ with:
50
+ name: python-package-distributions
51
+ path: dist/
52
+ - name: Publish package distributions to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1
54
+ with:
55
+ packages-dir: dist/
56
+
57
+ update-homebrew-tap:
58
+ name: Update Homebrew tap
59
+ runs-on: ubuntu-latest
60
+ needs: publish
61
+ concurrency:
62
+ group: homebrew-tap-update
63
+ cancel-in-progress: false
64
+ permissions:
65
+ contents: read
66
+
67
+ steps:
68
+ - name: Fetch sdist info from PyPI (with retry)
69
+ env:
70
+ VERSION: ${{ github.ref_name }}
71
+ run: |
72
+ VERSION="${VERSION#v}"
73
+ SHA256=""
74
+ for i in 1 2 3; do
75
+ RESPONSE=$(curl -sf "https://pypi.org/pypi/mdfetch/${VERSION}/json" || true)
76
+ if [ -n "$RESPONSE" ]; then
77
+ SDIST_URL=$(echo "$RESPONSE" | python3 -c \
78
+ "import sys,json; d=json.load(sys.stdin); \
79
+ urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
80
+ print(urls[0]['url'] if urls else '')" 2>/dev/null || true)
81
+ SHA256=$(echo "$RESPONSE" | python3 -c \
82
+ "import sys,json; d=json.load(sys.stdin); \
83
+ urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
84
+ print(urls[0]['digests']['sha256'] if urls else '')" 2>/dev/null || true)
85
+ if [ -n "$SHA256" ] && [ -n "$SDIST_URL" ]; then break; fi
86
+ fi
87
+ echo "PyPI not ready, retrying in 30s (attempt $i/3)..."
88
+ sleep 30
89
+ done
90
+ if [ -z "$SHA256" ]; then
91
+ echo "ERROR: Could not fetch sdist info from PyPI after 3 retries" >&2; exit 1
92
+ fi
93
+ echo "SDIST_URL=${SDIST_URL}" >> "$GITHUB_ENV"
94
+ echo "SHA256=${SHA256}" >> "$GITHUB_ENV"
95
+ echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
96
+
97
+ - name: Clone homebrew-tap and update formula
98
+ env:
99
+ TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
100
+ run: |
101
+ git clone "https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/stn1slv/homebrew-tap.git"
102
+ cd homebrew-tap
103
+ git config user.name "github-actions[bot]"
104
+ git config user.email "github-actions[bot]@users.noreply.github.com"
105
+
106
+ sed -i "s| url \"https://files.pythonhosted.org/packages/.*/mdfetch-.*\.tar\.gz\"| url \"${SDIST_URL}\"|" \
107
+ Formula/md-fetch.rb
108
+ sed -i "0,/ sha256 \"[a-f0-9]*\"/{s| sha256 \"[a-f0-9]*\"| sha256 \"${SHA256}\"|}" Formula/md-fetch.rb
109
+
110
+ if ! grep -qF " url \"${SDIST_URL}\"" Formula/md-fetch.rb; then
111
+ echo "ERROR: URL substitution made no change — formula URL pattern may have drifted" >&2; exit 1
112
+ fi
113
+ if ! grep -qF " sha256 \"${SHA256}\"" Formula/md-fetch.rb; then
114
+ echo "ERROR: SHA256 substitution made no change — formula sha256 pattern may have drifted" >&2; exit 1
115
+ fi
116
+
117
+ git add Formula/md-fetch.rb
118
+ git commit -m "chore: bump md-fetch to v${VERSION}"
119
+ git pull --rebase
120
+ git push
@@ -0,0 +1,3 @@
1
+ {
2
+ "feature_directory": "specs/009-homebrew-tap-formula"
3
+ }
@@ -1,3 +1,23 @@
1
+ ### mdfetch — Homebrew Tap Formula — 2026-05-16
2
+
3
+ **Branch**: `009-homebrew-tap-formula`
4
+ **Spec**: specs/009-homebrew-tap-formula
5
+
6
+ **What was added**:
7
+ - `Formula/md-fetch.rb` in `stn1slv/homebrew-tap` using `Language::Python::Virtualenv` pattern with 13 resource blocks; `depends_on "python@3.12"`; `uses_from_macos "libxml2"` and `uses_from_macos "libxslt"` (required by lxml); test block verifying `md-fetch --version`
8
+ - `update-homebrew-tap` job in `.github/workflows/publish.yml`: polls PyPI JSON API (3×30s retry), patches formula url+sha256 via sed, commits and pushes to tap; `concurrency: group=homebrew-tap-update, cancel-in-progress: false` serializes concurrent release runs
9
+ - `README.md` updated with `brew install stn1slv/tap/md-fetch` as secondary install option beneath `pip install mdfetch`
10
+ - `homebrew-tap/README.md` updated with md-fetch row in Available Formulas table
11
+ - TAP_GITHUB_TOKEN documented as required fine-grained PAT (`Contents: read+write` on `stn1slv/homebrew-tap` only)
12
+
13
+ **New Components**:
14
+ - `Formula/md-fetch.rb` (in `stn1slv/homebrew-tap`) — Homebrew formula
15
+ - `update-homebrew-tap` CI job (in `.github/workflows/publish.yml`)
16
+
17
+ **Tasks Completed**: 13/15 (T002 manual PAT setup, T015 post-merge verification pending)
18
+
19
+ ---
20
+
1
21
  ### mdfetch — Architecture Refactoring & CLI Improvements — 2026-05-16
2
22
 
3
23
  **Branch**: `refactor/reduce-duplication-and-robustness`
@@ -165,8 +165,46 @@ tests/
165
165
  specs/ # Speckit feature specifications
166
166
  pyproject.toml # hatchling build backend, uv package manager
167
167
  Makefile # setup / test / integration / lint / typecheck / format / build / clean / upgrade-deps
168
+ .github/workflows/
169
+ ├── ci.yml # lint + unit tests on push/PR (Python 3.12–3.14)
170
+ ├── integration.yml # scheduled integration tests
171
+ └── publish.yml # PyPI publish + Homebrew tap update on release tag
168
172
  ```
169
173
 
174
+ **External repository: `stn1slv/homebrew-tap`** [009-homebrew-tap-formula]
175
+ ```
176
+ Formula/
177
+ └── md-fetch.rb # Language::Python::Virtualenv formula; 13 resource blocks;
178
+ # depends_on "python@3.12"; uses_from_macos "libxml2" + "libxslt"
179
+ README.md # Available Formulas table updated with md-fetch row
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Homebrew Distribution Architecture [009-homebrew-tap-formula]
185
+
186
+ ```
187
+ GitHub Release tag vX.Y.Z
188
+ ↓
189
+ build job → publish job (PyPI)
190
+ ↓
191
+ update-homebrew-tap job (NEW, needs: publish)
192
+ [concurrency: group=homebrew-tap-update, cancel-in-progress=false]
193
+ 1. Poll https://pypi.org/pypi/mdfetch/{VERSION}/json (retry 3× / 30s)
194
+ 2. Extract SDIST_URL and SHA256
195
+ 3. Clone stn1slv/homebrew-tap via TAP_GITHUB_TOKEN (fine-grained PAT)
196
+ 4. sed patch Formula/md-fetch.rb (url + first sha256 only)
197
+ 5. git commit + pull --rebase + push
198
+ ```
199
+
200
+ **Formula pattern**: `Language::Python::Virtualenv` — creates isolated venv in Cellar, installs 13 resource blocks, symlinks `libexec/bin/md-fetch` → `bin/md-fetch`.
201
+
202
+ **System library dependencies** (required by `lxml` resource):
203
+ - `uses_from_macos "libxml2"`
204
+ - `uses_from_macos "libxslt"`
205
+
206
+ **TAP_GITHUB_TOKEN**: Fine-grained PAT with `Contents: read+write` on `stn1slv/homebrew-tap` only. Stored as repository secret in `stn1slv/md-fetch`. Must NOT be a classic `repo`-scoped PAT.
207
+
170
208
  ---
171
209
 
172
210
  ## Constraints
@@ -261,4 +299,4 @@ Makefile # setup / test / integration / lint / typecheck / f
261
299
 
262
300
  ---
263
301
 
264
- *Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/plan.md], [specs/005-substack-provider/plan.md], [specs/006-thenewstack-provider/plan.md]*
302
+ *Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/plan.md], [specs/005-substack-provider/plan.md], [specs/006-thenewstack-provider/plan.md], [specs/009-homebrew-tap-formula/plan.md]*
@@ -175,6 +175,43 @@ A developer passes a thenewstack.io URL that does not point to an article (e.g.,
175
175
 
176
176
  ---
177
177
 
178
+ ### US-016 — Install md-fetch via Homebrew (P1)
179
+ [Source: specs/009-homebrew-tap-formula]
180
+
181
+ A macOS or Linux developer who does not want to manage a Python environment installs the `md-fetch` CLI using a single Homebrew command. After installation, the `md-fetch` binary is immediately available on the PATH without any additional configuration.
182
+
183
+ **Acceptance Scenarios**:
184
+ 1. Given a macOS or Linux machine with Homebrew installed, when the user runs `brew install stn1slv/tap/md-fetch`, then the installation completes without errors and `md-fetch` is available on the PATH.
185
+ 2. Given a successful installation, when the user runs `md-fetch --version`, then the command exits with code 0 and outputs the installed package version.
186
+ 3. Given a successful installation, when the user runs `md-fetch <url>` with a supported URL, then the command returns Markdown output, confirming all dependencies are correctly bundled.
187
+ 4. Given an existing installation, when the user runs `brew upgrade stn1slv/tap/md-fetch`, then the installation upgrades to the latest published version.
188
+
189
+ ---
190
+
191
+ ### US-017 — Formula Stays in Sync with PyPI Releases (P2)
192
+ [Source: specs/009-homebrew-tap-formula]
193
+
194
+ When a new version of `md-fetch` is published to PyPI, the Homebrew formula in `stn1slv/homebrew-tap` is updated automatically without any manual intervention from the maintainer. Users running `brew upgrade` receive the new version promptly.
195
+
196
+ **Acceptance Scenarios**:
197
+ 1. Given a new version is successfully published to PyPI, when the release pipeline completes, then the formula in `stn1slv/homebrew-tap` reflects the new version number and correct integrity checksum.
198
+ 2. Given a new version is successfully published to PyPI, when the release pipeline completes, then the tap update is committed and pushed without any manual step from the maintainer.
199
+ 3. Given the automated tap update fails (e.g., token invalid, network error), when the pipeline completes, then the failure is reported as a visible CI job failure.
200
+ 4. Given a published package, when a user on the previous formula version runs `brew upgrade`, then they receive the newly published version.
201
+
202
+ ---
203
+
204
+ ### US-018 — Discover Homebrew Installation via Documentation (P3)
205
+ [Source: specs/009-homebrew-tap-formula]
206
+
207
+ A new user reading the project README discovers that Homebrew is an installation option on macOS and can install `md-fetch` without reading further instructions.
208
+
209
+ **Acceptance Scenarios**:
210
+ 1. Given the updated README, when a user visits the install section, then `brew install stn1slv/tap/md-fetch` is present as an installation option below the existing `pip install mdfetch` instruction.
211
+ 2. Given the README install section, when a user follows the brew instruction verbatim, then the installation succeeds.
212
+
213
+ ---
214
+
178
215
  ### US-006 — Integration Tests Pass Against Real dev.to Article URLs (P3)
179
216
  [Source: specs/002-devto-provider]
180
217
 
@@ -247,6 +284,17 @@ A developer runs the integration test suite and all dev.to integration tests pas
247
284
  - **FR-049**: The library MUST convert embedded third-party content (e.g., YouTube video iframes) in thenewstack.io articles to plain anchor links using the embed's source URL, discarding the embed wrapper — matching the pattern used by existing providers. [Source: specs/006-thenewstack-provider]
248
285
  - **FR-050**: The library MUST treat sponsored and native-advertising thenewstack.io article pages identically to editorial articles — extracting content as-is with no special detection, marking, or rejection. [Source: specs/006-thenewstack-provider]
249
286
 
287
+ ### Homebrew Distribution
288
+ - **FR-051**: A Homebrew formula for the `md-fetch` package MUST be available in the `stn1slv/homebrew-tap` repository so that users can install the tool with standard Homebrew commands. [Source: specs/009-homebrew-tap-formula]
289
+ - **FR-052**: The formula MUST install a working `md-fetch` binary into Homebrew's `bin/` directory, making it immediately available on the user's PATH after installation. [Source: specs/009-homebrew-tap-formula]
290
+ - **FR-053**: The formula MUST bundle all runtime dependencies so that no external Python environment or manual dependency installation is required. [Source: specs/009-homebrew-tap-formula]
291
+ - **FR-054**: The formula MUST include a `brew test` block that verifies the installed binary runs successfully (`md-fetch --version` exits with code 0). [Source: specs/009-homebrew-tap-formula]
292
+ - **FR-055**: When a new version is published to PyPI, the release pipeline MUST automatically update the formula with the new version and correct integrity checksum. The checksum fetch MUST retry up to 3 times with 30-second intervals before failing. [Source: specs/009-homebrew-tap-formula]
293
+ - **FR-056**: The automated formula update MUST commit and push to `stn1slv/homebrew-tap` using a dedicated fine-grained PAT (`Contents: read+write` on that repository only). [Source: specs/009-homebrew-tap-formula]
294
+ - **FR-057**: If the automated formula update step fails for any reason, the failure MUST surface as a failed CI job, preventing silent divergence between PyPI and Homebrew. [Source: specs/009-homebrew-tap-formula]
295
+ - **FR-058**: The project README MUST include the `brew install stn1slv/tap/md-fetch` command as a secondary install option beneath `pip install mdfetch`. [Source: specs/009-homebrew-tap-formula]
296
+ - **FR-059**: The `TAP_GITHUB_TOKEN` secret (a fine-grained PAT with `Contents: read+write` on `stn1slv/homebrew-tap`) must be documented as a required repository secret for the automation to function. [Source: specs/009-homebrew-tap-formula]
297
+
250
298
  ### dev.to Platform
251
299
  - **FR-015**: The library MUST add `dev.to` to the provider router so that any URL with the `dev.to` domain is dispatched to the dev.to provider without any change to the caller's code. [Source: specs/002-devto-provider]
252
300
  - **FR-016**: The library MUST include a dev.to provider that fetches the article page, isolates the main article body from `<div id="article-body">`, removes all non-content elements (navigation, social reaction widgets, comments, author sidebar, tag links), and returns the body as Markdown. [Source: specs/002-devto-provider]
@@ -304,6 +352,23 @@ A developer runs the integration test suite and all dev.to integration tests pas
304
352
 
305
353
  **Validation**: Page must contain `div#tns-post-body-content`; absent → `UnsupportedContentTypeError`. Body must yield non-empty text after stripping → else `EmptyContentError`. No paywall — all thenewstack.io articles are publicly accessible.
306
354
 
355
+ ### Homebrew Formula
356
+ [Source: specs/009-homebrew-tap-formula]
357
+ | Attribute | Type | Description |
358
+ |-----------|------|-------------|
359
+ | `url` | `str` | PyPI sdist tarball URL, changes on every release |
360
+ | `sha256` | `str` | Hex SHA-256 of the sdist tarball, changes on every release |
361
+ | `depends_on` | `str` | `"python@3.12"` (static) |
362
+ | `uses_from_macos` | `list[str]` | `["libxml2", "libxslt"]` — required by `lxml` resource |
363
+ | `resource` blocks | 13 entries | All runtime dependencies pinned to versions in `uv.lock` at formula creation time |
364
+
365
+ **Mutable fields per release**: `url` and `sha256` only. All `resource` blocks are static until a separate manual dependency-bump PR.
366
+
367
+ ### TAP_GITHUB_TOKEN
368
+ [Source: specs/009-homebrew-tap-formula]
369
+
370
+ A GitHub fine-grained Personal Access Token with `Contents: read+write` on `stn1slv/homebrew-tap` only. Stored as a repository secret in `stn1slv/md-fetch` and consumed exclusively by the `update-homebrew-tap` CI job.
371
+
307
372
  ### ExtractionResult (Output)
308
373
  | Attribute | Type | Description |
309
374
  |-----------|------|-------------|
@@ -379,6 +444,12 @@ caller provides URL string
379
444
  - **thenewstack.io sponsored content**: `div.tns-sponsor-note` (mid-article sponsor injection) and three disclosure div variants are inside `div#tns-post-body-content` and must be decomposed before conversion. Sponsored article pages are extracted identically to editorial articles (FR-050). [Source: specs/006-thenewstack-provider]
380
445
  - **thenewstack.io deck element**: `div.post-excerpt` is a `<div>`, not a semantic subtitle element; the extractor creates a new `<p>` tag with the deck text rather than copying the div directly, to ensure proper paragraph rendering. [Source: specs/006-thenewstack-provider]
381
446
  - **thenewstack.io HTML structure changes**: If the site redesign moves content outside `div#tns-post-body-content`, the extractor will require an update.
447
+ - **Homebrew tap temporarily unavailable**: If `stn1slv/homebrew-tap` is unreachable when the pipeline tries to push, `git push` fails and the CI job surfaces a failure — the maintainer must retry manually. [Source: specs/009-homebrew-tap-formula]
448
+ - **TAP_GITHUB_TOKEN expired or revoked**: `git clone` or `git push` exits non-zero → CI job fails → maintainer is alerted to rotate the token. [Source: specs/009-homebrew-tap-formula]
449
+ - **PyPI release yanked after formula update**: The formula is not automatically rolled back; the maintainer handles yanked-release scenarios manually. [Source: specs/009-homebrew-tap-formula]
450
+ - **Concurrent releases**: Addressed by `concurrency: group=homebrew-tap-update, cancel-in-progress=false` — runs are serialized so the second release waits for the first tap-update to complete. [Source: specs/009-homebrew-tap-formula]
451
+ - **brew test fails after install**: `brew test md-fetch` exits non-zero when a transitive dependency is missing or `md-fetch --version` fails — installation validation fails. [Source: specs/009-homebrew-tap-formula]
452
+ - **Formula structure changed (sed no-op)**: If the formula is manually edited and the url/sha256 line format changes, `sed` produces no diff and `git commit` finds nothing staged → exits non-zero → CI job fails. [Source: specs/009-homebrew-tap-formula]
382
453
 
383
454
  ---
384
455
 
@@ -409,6 +480,12 @@ caller provides URL string
409
480
  - **SC-029**: The extracted Markdown for any thenewstack.io article contains no consecutive blank-line runs of three or more lines. [Source: specs/006-thenewstack-provider]
410
481
  - **SC-030**: The TheNewStack provider is exercised by integration tests using real network requests against all five reference article URLs, matching the pattern established by existing providers. [Source: specs/006-thenewstack-provider]
411
482
 
483
+ - **SC-031**: A user with Homebrew installed can complete the full `md-fetch` installation using a single command, with no Python environment setup required, in under 3 minutes on a standard broadband connection. [Source: specs/009-homebrew-tap-formula]
484
+ - **SC-032**: The Homebrew formula's built-in test (`brew test md-fetch`) passes on macOS and Linux after every installation. [Source: specs/009-homebrew-tap-formula]
485
+ - **SC-033**: The formula in `stn1slv/homebrew-tap` is updated within 10 minutes of PyPI publish confirmation — no manual maintainer action is required. [Source: specs/009-homebrew-tap-formula]
486
+ - **SC-034**: Formula update failures produce a visible CI job failure on every failed attempt, with zero silent failures. [Source: specs/009-homebrew-tap-formula]
487
+ - **SC-035**: The README install section includes the Homebrew installation command, enabling users to discover and use it without prior knowledge of the project's Python packaging. [Source: specs/009-homebrew-tap-formula]
488
+
412
489
  ---
413
490
 
414
491
  ## Assumptions
@@ -428,7 +505,12 @@ caller provides URL string
428
505
  - **SC-015**: Zero changes are required in existing caller code to benefit from the Freedium fallback — existing integrations continue to work as-is. [Source: specs/003-medium-freedium-fallback]
429
506
  - **SC-016**: When the primary Medium request succeeds, there is no additional latency attributable to the fallback mechanism. [Source: specs/003-medium-freedium-fallback]
430
507
  - **SC-017**: All existing unit and integration tests for the Medium extractor continue to pass without modification after the fallback is introduced. [Source: specs/003-medium-freedium-fallback]
508
+ - Homebrew is already installed on the user's machine; the feature does not cover installing Homebrew itself. [Source: specs/009-homebrew-tap-formula]
509
+ - The formula targets the standard Homebrew installation on both macOS and Linux; Windows (WSL) is out of scope. [Source: specs/009-homebrew-tap-formula]
510
+ - Only the source distribution (sdist) is used as the formula's source artifact; binary wheels are not referenced by the formula. [Source: specs/009-homebrew-tap-formula]
511
+ - A `TAP_GITHUB_TOKEN` PAT will be created and added to repository secrets by the maintainer before first use — this is a one-time manual prerequisite. [Source: specs/009-homebrew-tap-formula]
512
+ - If a PyPI release is yanked after the formula has been updated, the formula is not automatically rolled back. [Source: specs/009-homebrew-tap-formula]
431
513
 
432
514
  ---
433
515
 
434
- *Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/spec.md], [specs/005-substack-provider/spec.md], [specs/006-thenewstack-provider/spec.md]*
516
+ *Last Updated: 2026-05-16 | Sources appended: [specs/004-remove-backoff/spec.md], [specs/005-substack-provider/spec.md], [specs/006-thenewstack-provider/spec.md], [specs/009-homebrew-tap-formula/spec.md]*
@@ -55,10 +55,6 @@ make typecheck # type check
55
55
  ```
56
56
 
57
57
  <!-- SPECKIT START -->
58
- ## Active Feature Plan
59
-
60
- Active feature: `008-mdfetch-cli`. Implementation plan: [`specs/008-mdfetch-cli/plan.md`](specs/008-mdfetch-cli/plan.md).
61
-
62
58
  ## Known Issues & Gotchas
63
59
 
64
60
  ### ⚠️ test_router.py "unsupported domain" fixture must be updated per new provider
@@ -75,4 +71,9 @@ Active feature: `008-mdfetch-cli`. Implementation plan: [`specs/008-mdfetch-cli/
75
71
  **Issue:** VoxPop poll widgets (`div.tns-voxpop-screen`) appear to be inline article content but are actually page-level overlay modals.
76
72
  **Root Cause:** VoxPop is injected outside `div#tns-post-body-content` — confirmed via live DOM inspection across all 5 reference articles.
77
73
  **Prevention Rule:** Do not add VoxPop stripping to `TheNewStackExtractor.clean_html()`. If a future redesign moves VoxPop inside the body container, update `clean_html()` to decompose `div.tns-voxpop-screen`.
74
+
75
+ ### ⚠️ Homebrew formula requires `uses_from_macos` for lxml
76
+ **Issue:** `brew audit --strict --new Formula/md-fetch.rb` fails with missing system library declarations when `lxml` is a resource block.
77
+ **Root Cause:** `lxml` requires `libxml2` and `libxslt`, which are macOS system libraries. Homebrew requires these to be declared explicitly via `uses_from_macos`.
78
+ **Prevention Rule:** Any Homebrew formula that includes `lxml` as a resource MUST declare `uses_from_macos "libxml2"` and `uses_from_macos "libxslt"` after the `depends_on` lines. Discovered via `brew audit --strict --new` during implementation.
78
79
  <!-- SPECKIT END -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdfetch
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Extract article content from web platforms and return it as clean Markdown.
5
5
  Project-URL: Homepage, https://github.com/stn1slv/md-fetch
6
6
  Project-URL: Source, https://github.com/stn1slv/md-fetch
@@ -37,10 +37,20 @@ A Python library that extracts article content from web platforms and returns it
37
37
 
38
38
  ## Install
39
39
 
40
+ ### pip
41
+
40
42
  ```bash
41
43
  pip install mdfetch
42
44
  ```
43
45
 
46
+ ### Homebrew (macOS / Linux)
47
+
48
+ ```bash
49
+ brew install stn1slv/tap/md-fetch
50
+ ```
51
+
52
+ No Python environment setup required.
53
+
44
54
  ## CLI Usage
45
55
 
46
56
  You can use the built-in `md-fetch` command directly from your terminal:
@@ -4,10 +4,20 @@ A Python library that extracts article content from web platforms and returns it
4
4
 
5
5
  ## Install
6
6
 
7
+ ### pip
8
+
7
9
  ```bash
8
10
  pip install mdfetch
9
11
  ```
10
12
 
13
+ ### Homebrew (macOS / Linux)
14
+
15
+ ```bash
16
+ brew install stn1slv/tap/md-fetch
17
+ ```
18
+
19
+ No Python environment setup required.
20
+
11
21
  ## CLI Usage
12
22
 
13
23
  You can use the built-in `md-fetch` command directly from your terminal:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mdfetch"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Extract article content from web platforms and return it as clean Markdown."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,36 @@
1
+ # Specification Quality Checklist: Homebrew Tap Formula
2
+
3
+ **Purpose**: Validate specification completeness and quality before proceeding to planning
4
+ **Created**: 2026-05-16
5
+ **Feature**: [spec.md](../spec.md)
6
+
7
+ ## Content Quality
8
+
9
+ - [x] No implementation details (languages, frameworks, APIs)
10
+ - [x] Focused on user value and business needs
11
+ - [x] Written for non-technical stakeholders
12
+ - [x] All mandatory sections completed
13
+
14
+ ## Requirement Completeness
15
+
16
+ - [x] No [NEEDS CLARIFICATION] markers remain
17
+ - [x] Requirements are testable and unambiguous
18
+ - [x] Success criteria are measurable
19
+ - [x] Success criteria are technology-agnostic (no implementation details)
20
+ - [x] All acceptance scenarios are defined
21
+ - [x] Edge cases are identified
22
+ - [x] Scope is clearly bounded
23
+ - [x] Dependencies and assumptions identified
24
+
25
+ ## Feature Readiness
26
+
27
+ - [x] All functional requirements have clear acceptance criteria
28
+ - [x] User scenarios cover primary flows
29
+ - [x] Feature meets measurable outcomes defined in Success Criteria
30
+ - [x] No implementation details leak into specification
31
+
32
+ ## Notes
33
+
34
+ - All items pass. Spec is ready for `/speckit-plan`.
35
+ - The formula's `Language::Python::Virtualenv` pattern and SHA256 mechanics are deliberately kept out of the spec (they are implementation decisions captured in the plan).
36
+ - The `TAP_GITHUB_TOKEN` prerequisite is documented as an assumption since it requires a one-time manual step outside the automated workflow.
@@ -0,0 +1,119 @@
1
+ # Contract: Homebrew Formula (`Formula/md-fetch.rb`)
2
+
3
+ **Repository**: `stn1slv/homebrew-tap`
4
+ **Date**: 2026-05-16
5
+
6
+ ## Purpose
7
+
8
+ Defines the complete structure of the `md-fetch` Homebrew formula so that the implementation can be written without ambiguity.
9
+
10
+ ## Formula Skeleton
11
+
12
+ ```ruby
13
+ # typed: false
14
+ # frozen_string_literal: true
15
+
16
+ class MdFetch < Formula
17
+ include Language::Python::Virtualenv
18
+
19
+ desc "Extract article content from web platforms and return it as clean Markdown"
20
+ homepage "https://github.com/stn1slv/md-fetch"
21
+ url "https://files.pythonhosted.org/packages/<hash-path>/mdfetch-0.5.0.tar.gz"
22
+ sha256 "<mdfetch-0.5.0-sdist-sha256>"
23
+ license "MIT"
24
+
25
+ depends_on "python@3.12"
26
+
27
+ uses_from_macos "libxml2"
28
+ uses_from_macos "libxslt"
29
+
30
+ resource "anyio" do
31
+ url "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz"
32
+ sha256 "334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"
33
+ end
34
+
35
+ resource "beautifulsoup4" do
36
+ url "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz"
37
+ sha256 "6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"
38
+ end
39
+
40
+ resource "certifi" do
41
+ url "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz"
42
+ sha256 "8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"
43
+ end
44
+
45
+ resource "click" do
46
+ url "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz"
47
+ sha256 "398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"
48
+ end
49
+
50
+ resource "h11" do
51
+ url "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz"
52
+ sha256 "4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"
53
+ end
54
+
55
+ resource "httpcore" do
56
+ url "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz"
57
+ sha256 "6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"
58
+ end
59
+
60
+ resource "httpx" do
61
+ url "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz"
62
+ sha256 "75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"
63
+ end
64
+
65
+ resource "idna" do
66
+ url "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz"
67
+ sha256 "ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"
68
+ end
69
+
70
+ resource "lxml" do
71
+ url "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz"
72
+ sha256 "bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13"
73
+ end
74
+
75
+ resource "markdownify" do
76
+ url "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz"
77
+ sha256 "b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09"
78
+ end
79
+
80
+ resource "six" do
81
+ url "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz"
82
+ sha256 "ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
83
+ end
84
+
85
+ resource "soupsieve" do
86
+ url "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz"
87
+ sha256 "3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"
88
+ end
89
+
90
+ resource "typing-extensions" do
91
+ url "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz"
92
+ sha256 "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"
93
+ end
94
+
95
+ def install
96
+ virtualenv_install_with_resources
97
+ end
98
+
99
+ test do
100
+ assert_match version.to_s, shell_output("#{bin}/md-fetch --version")
101
+ end
102
+ end
103
+ ```
104
+
105
+ ## Invariants
106
+
107
+ - `url` and `sha256` (package) are the **only** fields updated on each mdfetch release.
108
+ - All `resource` blocks are updated only when runtime dependencies change (separate manual PR to homebrew-tap).
109
+ - The `test` block uses `assert_match version.to_s` so it automatically validates the installed binary reports the expected version.
110
+
111
+ ## Pre-Implementation Note
112
+
113
+ The `url` path component for `mdfetch-0.5.0.tar.gz` (the `<hash-path>` placeholder above) must be retrieved from PyPI before writing the initial formula. Run:
114
+
115
+ ```bash
116
+ curl -s https://pypi.org/pypi/mdfetch/0.5.0/json | \
117
+ python3 -c "import sys,json; d=json.load(sys.stdin); \
118
+ [print(u['url'], u['digests']['sha256']) for u in d['urls'] if u['packagetype']=='sdist']"
119
+ ```
@@ -0,0 +1,86 @@
1
+ # Contract: Tap-Update CI Job (`update-homebrew-tap`)
2
+
3
+ **Workflow file**: `.github/workflows/publish.yml` in `stn1slv/md-fetch`
4
+ **Date**: 2026-05-16
5
+
6
+ ## Job Summary
7
+
8
+ A new `update-homebrew-tap` job is added to `publish.yml`, running after the existing `publish` job succeeds. It fetches the new release's sdist SHA-256 from PyPI, clones the homebrew-tap repo, patches the formula, and pushes the commit.
9
+
10
+ ## Job Definition Skeleton
11
+
12
+ ```yaml
13
+ update-homebrew-tap:
14
+ name: Update Homebrew tap
15
+ runs-on: ubuntu-latest
16
+ needs: publish
17
+ concurrency:
18
+ group: homebrew-tap-update
19
+ cancel-in-progress: false
20
+ permissions:
21
+ contents: read
22
+
23
+ steps:
24
+ - name: Fetch sdist info from PyPI (with retry)
25
+ env:
26
+ VERSION: ${{ github.ref_name }} # e.g. v0.5.1
27
+ run: |
28
+ VERSION="${VERSION#v}" # strip leading 'v'
29
+ SHA256=""
30
+ for i in 1 2 3; do
31
+ RESPONSE=$(curl -sf "https://pypi.org/pypi/mdfetch/${VERSION}/json" || true)
32
+ if [ -n "$RESPONSE" ]; then
33
+ SDIST_URL=$(echo "$RESPONSE" | python3 -c \
34
+ "import sys,json; d=json.load(sys.stdin); \
35
+ urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
36
+ print(urls[0]['url'] if urls else '')" 2>/dev/null || true)
37
+ SHA256=$(echo "$RESPONSE" | python3 -c \
38
+ "import sys,json; d=json.load(sys.stdin); \
39
+ urls=[u for u in d['urls'] if u['packagetype']=='sdist']; \
40
+ print(urls[0]['digests']['sha256'] if urls else '')" 2>/dev/null || true)
41
+ if [ -n "$SHA256" ] && [ -n "$SDIST_URL" ]; then break; fi
42
+ fi
43
+ echo "PyPI not ready, retrying in 30s (attempt $i/3)..."
44
+ sleep 30
45
+ done
46
+ if [ -z "$SHA256" ]; then
47
+ echo "ERROR: Could not fetch sdist info from PyPI after 3 retries" >&2; exit 1
48
+ fi
49
+ echo "SDIST_URL=${SDIST_URL}" >> "$GITHUB_ENV"
50
+ echo "SHA256=${SHA256}" >> "$GITHUB_ENV"
51
+ echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
52
+
53
+ - name: Clone homebrew-tap and update formula
54
+ env:
55
+ TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
56
+ run: |
57
+ git clone "https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/stn1slv/homebrew-tap.git"
58
+ cd homebrew-tap
59
+ git config user.name "github-actions[bot]"
60
+ git config user.email "github-actions[bot]@users.noreply.github.com"
61
+
62
+ # Replace the main package url and sha256 lines
63
+ sed -i "s| url \"https://files.pythonhosted.org/packages/.*/mdfetch-.*\.tar\.gz\"| url \"${SDIST_URL}\"|" \
64
+ Formula/md-fetch.rb
65
+ sed -i "0,/ sha256 \"[a-f0-9]*\"/{s| sha256 \"[a-f0-9]*\"| sha256 \"${SHA256}\"|}" Formula/md-fetch.rb
66
+
67
+ git add Formula/md-fetch.rb
68
+ git commit -m "chore: bump md-fetch to v${VERSION}"
69
+ git pull --rebase
70
+ git push
71
+ ```
72
+
73
+ ## Failure Modes and Expected Behavior
74
+
75
+ | Failure | Job Behaviour |
76
+ |---------|--------------|
77
+ | PyPI returns 404 for new version after 3×30s retries | Step exits non-zero → job fails → workflow shows red |
78
+ | `TAP_GITHUB_TOKEN` absent or invalid | `git clone` or `git push` exits non-zero → job fails |
79
+ | `sed` finds no matching line (formula structure changed) | `git diff --quiet` would show no changes; `git commit` would fail — job exits non-zero |
80
+ | Concurrent release overlap | `concurrency: group=homebrew-tap-update` serializes runs; second run queues and proceeds after first completes (`cancel-in-progress: false`) |
81
+
82
+ ## Prerequisites
83
+
84
+ - `TAP_GITHUB_TOKEN` secret must be set in `stn1slv/md-fetch` repository settings.
85
+ - Token must be a **fine-grained PAT** with `Contents: read+write` on `stn1slv/homebrew-tap` only. Do not use a classic `repo`-scoped PAT.
86
+ - `jq` is **not** required — Python 3 (always available on `ubuntu-latest`) is used for JSON parsing.