mdfetch 0.2.1__tar.gz → 0.2.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 (142) hide show
  1. {mdfetch-0.2.1 → mdfetch-0.2.2}/.github/workflows/integration.yml +0 -3
  2. mdfetch-0.2.2/.specify/feature.json +3 -0
  3. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/changelog.md +17 -0
  4. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/plan.md +7 -5
  5. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/spec.md +20 -4
  6. {mdfetch-0.2.1 → mdfetch-0.2.2}/CLAUDE.md +3 -1
  7. {mdfetch-0.2.1 → mdfetch-0.2.2}/PKG-INFO +1 -1
  8. {mdfetch-0.2.1 → mdfetch-0.2.2}/pyproject.toml +1 -1
  9. mdfetch-0.2.2/specs/004-remove-backoff/checklists/requirements.md +34 -0
  10. mdfetch-0.2.2/specs/004-remove-backoff/plan.md +146 -0
  11. mdfetch-0.2.2/specs/004-remove-backoff/research.md +42 -0
  12. mdfetch-0.2.2/specs/004-remove-backoff/spec.md +92 -0
  13. mdfetch-0.2.2/specs/004-remove-backoff/tasks.md +147 -0
  14. {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/__init__.py +2 -2
  15. {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/base.py +6 -7
  16. {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/providers/medium.py +31 -1
  17. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/architecting-the-asynchronous-agent.md +97 -146
  18. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/from-drift-to-parity.md +19 -19
  19. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/integration-digest-december-2025.md +5 -5
  20. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/test_devto_integration.py +2 -7
  21. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/test_medium_integration.py +2 -7
  22. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_fetch_errors.py +3 -24
  23. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_medium_extractor.py +29 -0
  24. {mdfetch-0.2.1 → mdfetch-0.2.2}/uv.lock +1 -1
  25. mdfetch-0.2.1/.specify/feature.json +0 -3
  26. mdfetch-0.2.1/tests/integration/conftest.py +0 -17
  27. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-analyze/SKILL.md +0 -0
  28. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-archive-run/SKILL.md +0 -0
  29. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-checklist/SKILL.md +0 -0
  30. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-clarify/SKILL.md +0 -0
  31. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-constitution/SKILL.md +0 -0
  32. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
  33. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
  34. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
  35. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
  36. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
  37. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-implement/SKILL.md +0 -0
  38. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-plan/SKILL.md +0 -0
  39. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-reconcile-run/SKILL.md +0 -0
  40. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-specify/SKILL.md +0 -0
  41. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-tasks/SKILL.md +0 -0
  42. {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
  43. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.analyze.toml +0 -0
  44. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.archive.run.toml +0 -0
  45. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.checklist.toml +0 -0
  46. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.clarify.toml +0 -0
  47. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.constitution.toml +0 -0
  48. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.implement.toml +0 -0
  49. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.plan.toml +0 -0
  50. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.reconcile.run.toml +0 -0
  51. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.specify.toml +0 -0
  52. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.tasks.toml +0 -0
  53. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.taskstoissues.toml +0 -0
  54. {mdfetch-0.2.1 → mdfetch-0.2.2}/.github/workflows/ci.yml +0 -0
  55. {mdfetch-0.2.1 → mdfetch-0.2.2}/.github/workflows/publish.yml +0 -0
  56. {mdfetch-0.2.1 → mdfetch-0.2.2}/.gitignore +0 -0
  57. {mdfetch-0.2.1 → mdfetch-0.2.2}/.python-version +0 -0
  58. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/.registry +0 -0
  59. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/LICENSE +0 -0
  60. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/README.md +0 -0
  61. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/commands/archive.md +0 -0
  62. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/extension.yml +0 -0
  63. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/README.md +0 -0
  64. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
  65. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
  66. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
  67. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
  68. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
  69. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/config-template.yml +0 -0
  70. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/extension.yml +0 -0
  71. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/git-config.yml +0 -0
  72. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
  73. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
  74. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
  75. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
  76. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
  77. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
  78. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
  79. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
  80. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/LICENSE +0 -0
  81. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/README.md +0 -0
  82. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/commands/reconcile.md +0 -0
  83. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/extension.yml +0 -0
  84. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions.yml +0 -0
  85. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/init-options.json +0 -0
  86. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integration.json +0 -0
  87. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integrations/claude.manifest.json +0 -0
  88. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integrations/gemini.manifest.json +0 -0
  89. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integrations/speckit.manifest.json +0 -0
  90. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/constitution.md +0 -0
  91. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  92. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/common.sh +0 -0
  93. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/create-new-feature.sh +0 -0
  94. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/setup-plan.sh +0 -0
  95. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/setup-tasks.sh +0 -0
  96. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/checklist-template.md +0 -0
  97. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/constitution-template.md +0 -0
  98. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/plan-template.md +0 -0
  99. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/spec-template.md +0 -0
  100. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/tasks-template.md +0 -0
  101. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/workflows/speckit/workflow.yml +0 -0
  102. {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/workflows/workflow-registry.json +0 -0
  103. {mdfetch-0.2.1 → mdfetch-0.2.2}/GEMINI.md +0 -0
  104. {mdfetch-0.2.1 → mdfetch-0.2.2}/LICENSE +0 -0
  105. {mdfetch-0.2.1 → mdfetch-0.2.2}/Makefile +0 -0
  106. {mdfetch-0.2.1 → mdfetch-0.2.2}/README.md +0 -0
  107. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/checklists/requirements.md +0 -0
  108. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/contracts/api.md +0 -0
  109. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/data-model.md +0 -0
  110. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/plan.md +0 -0
  111. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/quickstart.md +0 -0
  112. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/research.md +0 -0
  113. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/spec.md +0 -0
  114. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/tasks.md +0 -0
  115. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/checklists/requirements.md +0 -0
  116. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/contracts/public-api.md +0 -0
  117. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/data-model.md +0 -0
  118. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/plan.md +0 -0
  119. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/quickstart.md +0 -0
  120. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/research.md +0 -0
  121. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/spec.md +0 -0
  122. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/tasks.md +0 -0
  123. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/checklists/requirements.md +0 -0
  124. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/contracts/extract-api.md +0 -0
  125. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/plan.md +0 -0
  126. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/research.md +0 -0
  127. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/spec.md +0 -0
  128. {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/tasks.md +0 -0
  129. {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/exceptions.py +0 -0
  130. {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/providers/__init__.py +0 -0
  131. {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/providers/devto.py +0 -0
  132. {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/router.py +0 -0
  133. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/__init__.py +0 -0
  134. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/conftest.py +0 -0
  135. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/__init__.py +0 -0
  136. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/devto-integration-digest-december-2025.md +0 -0
  137. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/devto-integration-digest-july-2025.md +0 -0
  138. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/devto-integration-digest-march-2026.md +0 -0
  139. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/__init__.py +0 -0
  140. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_devto_extractor.py +0 -0
  141. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_router.py +0 -0
  142. {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_silent.py +0 -0
@@ -30,9 +30,6 @@ jobs:
30
30
 
31
31
  - name: Run integration tests
32
32
  id: integration
33
- env:
34
- MDFETCH_RETRIES: "6"
35
- MDFETCH_RETRY_DELAY: "2.0"
36
33
  run: make integration
37
34
 
38
35
  - name: Create issue on failure
@@ -0,0 +1,3 @@
1
+ {
2
+ "feature_directory": "specs/004-remove-backoff"
3
+ }
@@ -2,6 +2,23 @@
2
2
 
3
3
  ---
4
4
 
5
+ ### mdfetch — Remove Exponential Backoff — 2026-05-15
6
+
7
+ **Branch**: `004-remove-backoff`
8
+ **Spec**: specs/004-remove-backoff
9
+
10
+ **What was added**:
11
+ - Fixed-delay retry behaviour: `fetch_html` now sleeps exactly `retry_delay` seconds between attempts (previously exponential `retry_delay × 2ⁿ`, capped at 60 s). The change makes retry timing predictable for callers and aligns with the Freedium fallback that already absorbs 403/429 errors.
12
+ - Removed `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` env-var support from integration test fixtures (`conftest.py` deleted) and CI workflow (`integration.yml`). Integration tests now hardcode `retries=3, retry_delay=2.0`.
13
+ - Deleted two unit tests (`test_exponential_backoff_sleep_sequence`, `test_exponential_backoff_capped_at_max_delay`) that verified the removed exponential schedule. Added sleep-value assertion to `test_status_code_not_in_no_retry_set_still_retries` to close the FR-029 gap.
14
+
15
+ **New Components**:
16
+ - No new files. Pure removal: `tests/integration/conftest.py` deleted; `_MAX_RETRY_DELAY` constant removed from `src/mdfetch/base.py`.
17
+
18
+ **Tasks Completed**: 16/16
19
+
20
+ ---
21
+
5
22
  ### mdfetch — Medium Freedium Fallback — 2026-05-15
6
23
 
7
24
  **Branch**: `003-medium-freedium-fallback`
@@ -1,7 +1,7 @@
1
1
  # mdfetch — Main Implementation Plan
2
2
 
3
- **Last Updated**: 2026-05-14
4
- **Sources**: [specs/001-mdfetch-medium-extractor/plan.md], [specs/002-devto-provider/plan.md]
3
+ **Last Updated**: 2026-05-15
4
+ **Sources**: [specs/001-mdfetch-medium-extractor/plan.md], [specs/002-devto-provider/plan.md], [specs/003-medium-freedium-fallback/plan.md], [specs/004-remove-backoff/plan.md]
5
5
 
6
6
  ---
7
7
 
@@ -41,6 +41,7 @@ BaseExtractor (ABC) — src/mdfetch/base.py
41
41
  ├── _no_retry_status_codes: frozenset[int] = frozenset() — codes that skip retry; overridden by providers
42
42
  ├── fetch_html(url, *, retries, retry_delay, _no_retry_codes=None) → str
43
43
  │ — streaming HTTP, 30s timeout, 10 MB cap;
44
+ │ fixed delay of retry_delay seconds between attempts (not exponential);
44
45
  │ codes in _no_retry_codes (or class attribute) raise immediately
45
46
  ├── clean_html(soup) → Tag — abstract: platform-specific HTML isolation
46
47
  ├── convert_to_markdown(tag) → str— abstract: platform-specific Markdown conversion
@@ -139,7 +140,7 @@ Makefile # setup / test / integration / lint / typecheck / f
139
140
 
140
141
  ## Testing Strategy
141
142
 
142
- **Unit tests** (65 tests, offline):
143
+ **Unit tests** (63 tests, offline):
143
144
  - Router: domain routing, subdomain suffix matching, duplicate registration, invalid URLs, unsupported platforms
144
145
  - MediumExtractor: clean_html, convert_to_markdown, empty content, non-article pages, _parse_freedium (heading remap, missing main-content), fallback on 403/429 (URL construction, exc.url contract, no-sleep on 429), no-fallback on 200, UnsupportedContentTypeError.url on Freedium path [003-medium-freedium-fallback]
145
146
  - DevToExtractor: clean_html (title/cover/heading/image preservation, iframe/ltag embed→link, anchor stripping, non-article error), convert_to_markdown (headings/code/lists/images, no raw HTML, empty content error) [002-devto-provider]
@@ -150,7 +151,7 @@ Makefile # setup / test / integration / lint / typecheck / f
150
151
  - Parametrized over 3 real stn1slv.medium.com articles (including a known paywalled URL that exercises the Freedium fallback when medium.com returns 403) [003-medium-freedium-fallback]
151
152
  - Parametrized over 3 real dev.to/stn1slv articles [002-devto-provider]
152
153
  - Snapshot-based containment check: `expected_body in extracted_result` — tests pass regardless of whether medium.com or Freedium served the content (heading normalisation ensures identical output)
153
- - 3 retries with 2-second delay on `FetchError` (covers transient 403s/timeouts)
154
+ - 3 retries with 2-second **fixed** delay on `FetchError` (hardcoded; not env-var configurable) [004-remove-backoff]
154
155
  - Run with: `make integration` or `uv run pytest tests/integration/ --override-ini=addopts=`
155
156
  - Excluded from default `pytest` run via `addopts = "-m 'not integration'"` in pyproject.toml
156
157
 
@@ -189,6 +190,7 @@ Makefile # setup / test / integration / lint / typecheck / f
189
190
  | Medium 403/429 fallback | Override `extract()` in `MediumExtractor`; `_no_retry_status_codes=frozenset({403,429})` on class | Immediate fallback with no medium.com retries; `BaseExtractor` extended with `_no_retry_codes` param for thread safety | [003-medium-freedium-fallback]
190
191
  | Freedium HTML parsing | Dedicated `_parse_freedium()` method; `div.main-content`; h4→h3 remap | Freedium HTML is structurally incompatible with `clean_html()` (no `<article>`); heading remap ensures snapshot tests pass for both paths | [003-medium-freedium-fallback]
191
192
  | Freedium exc.url contract | `inner_exc.url = url` unconditionally; error message is source-agnostic ("Fallback page…") | Preserves transparent-fallback contract (FR-028); `exc.url` is the authoritative field; message content is internal | [003-medium-freedium-fallback]
193
+ | Retry strategy | Fixed delay (`retry_delay` seconds per attempt, unchanged between attempts) | Exponential backoff removed (PR #8); Freedium fallback absorbs 403/429 at a higher level making exponential growth unnecessary; integration tests use hardcoded defaults (`retries=3, retry_delay=2.0`) with no env-var override | [004-remove-backoff]
192
194
 
193
195
  ---
194
196
 
@@ -202,4 +204,4 @@ Makefile # setup / test / integration / lint / typecheck / f
202
204
 
203
205
  ---
204
206
 
205
- *Last Updated: 2026-05-15 | Sources appended: [specs/003-medium-freedium-fallback/plan.md]*
207
+ *Last Updated: 2026-05-15 | Sources appended: [specs/004-remove-backoff/plan.md]*
@@ -1,7 +1,7 @@
1
1
  # mdfetch — Main Specification
2
2
 
3
- **Last Updated**: 2026-05-14
4
- **Sources**: [specs/001-mdfetch-medium-extractor/spec.md], [specs/002-devto-provider/spec.md]
3
+ **Last Updated**: 2026-05-15
4
+ **Sources**: [specs/001-mdfetch-medium-extractor/spec.md], [specs/002-devto-provider/spec.md], [specs/003-medium-freedium-fallback/spec.md], [specs/004-remove-backoff/spec.md]
5
5
 
6
6
  ---
7
7
 
@@ -105,6 +105,18 @@ A developer calls the library's extract function for a publicly accessible Mediu
105
105
 
106
106
  ---
107
107
 
108
+ ### US-010 — Predictable Fixed-Delay Retry Behaviour (P1)
109
+ [Source: specs/004-remove-backoff]
110
+
111
+ A developer calling `extract()` encounters a transient network error. The library retries using a simple, predictable fixed delay — exactly `retry_delay` seconds between every attempt — rather than an exponentially growing delay. The retry timing is constant and easy to reason about regardless of which attempt number is being made.
112
+
113
+ **Acceptance Scenarios**:
114
+ 1. Given `fetch_html` is called with `retries=3` and `retry_delay=2.0`, when the first two attempts raise a transient error, then the library sleeps exactly `2.0` seconds before each retry (not `2.0` then `4.0`).
115
+ 2. Given `fetch_html` is called with `retries=1`, when the attempt fails, then no sleep occurs and the exception is raised immediately.
116
+ 3. Given a status code in `_no_retry_status_codes`, when that error is raised, then no sleep or retry occurs.
117
+
118
+ ---
119
+
108
120
  ### US-006 — Integration Tests Pass Against Real dev.to Article URLs (P3)
109
121
  [Source: specs/002-devto-provider]
110
122
 
@@ -135,6 +147,7 @@ A developer runs the integration test suite and all dev.to integration tests pas
135
147
  ### Network
136
148
  - **FR-006**: The library MUST raise a descriptive error when a network request fails (connection error, timeout, non-2xx HTTP status).
137
149
  - **FR-014**: HTTP requests MUST use a standard browser-like User-Agent string so that web servers return readable HTML. The User-Agent MUST NOT identify the library by name or version. The library does not check or respect `robots.txt` in v1.
150
+ - **FR-029**: The `fetch_html` method MUST use a fixed delay of exactly `retry_delay` seconds between retry attempts — no exponential multiplication. The retry delay is constant and does not grow with each attempt number. [Source: specs/004-remove-backoff]
138
151
 
139
152
  ### Packaging & Testing
140
153
  - **FR-009**: The library MUST be packaged and distributed via PyPI using modern Python packaging best practices, enabling installation through the standard package manager without additional steps.
@@ -238,7 +251,7 @@ caller provides URL string
238
251
  - **Network timeouts**: Covered by `FetchError` (30-second fixed timeout).
239
252
  - **Oversized responses**: Responses exceeding 10 MB are rejected with `FetchError` to prevent OOM.
240
253
  - **Profile/tag pages**: When a `medium.com` URL points to a non-article page, `UnsupportedContentTypeError` is raised (distinct from `UnsupportedPlatformError`).
241
- - **HTTP 403 / transient failures**: Integration tests use a 3-retry helper with 2-second delay to handle transient rate limits.
254
+ - **HTTP 403 / transient failures**: Integration tests use 3 retries with a 2-second fixed delay (hardcoded; not env-var configurable) to handle transient rate limits. [Source: specs/004-remove-backoff]
242
255
  - **dev.to profile pages**: When a `dev.to` URL points to an author profile (no `div#article-body`), `UnsupportedContentTypeError` is raised.
243
256
  - **dev.to tag listing pages**: When a `dev.to` URL points to a tag page (e.g., `dev.to/t/kafka`), `UnsupportedContentTypeError` is raised.
244
257
  - **dev.to liquid-tag embeds**: Embedded third-party widgets (GitHub Gists, CodePen, YouTube) serialised as `<div class="ltag__*" data-url="...">` are replaced with plain Markdown links; they are never silently dropped.
@@ -273,6 +286,9 @@ caller provides URL string
273
286
  - The library supports Python 3.12 and later.
274
287
  - The library operates on publicly accessible HTML; it does not execute JavaScript or render dynamic content.
275
288
  - Network timeouts use a fixed default of 30 seconds (not user-configurable in v1).
289
+ - **SC-018**: `make test` passes with zero failures (all unit tests green) when run without any `MDFETCH_*` environment variables. [Source: specs/004-remove-backoff]
290
+ - **SC-019**: `make integration` passes with zero failures when run without any `MDFETCH_RETRIES` or `MDFETCH_RETRY_DELAY` environment variables — integration tests use hardcoded defaults (3 retries, 2.0 s delay). [Source: specs/004-remove-backoff]
291
+ - **SC-020**: No reference to `MDFETCH_RETRIES`, `MDFETCH_RETRY_DELAY`, or `_MAX_RETRY_DELAY` appears in `src/`, `tests/`, or `.github/` (specification and documentation files excluded). [Source: specs/004-remove-backoff]
276
292
  - **SC-013**: Articles that previously failed with a 403 paywall error are successfully extracted in at least 90% of cases where the Freedium mirror has the content available. [Source: specs/003-medium-freedium-fallback]
277
293
  - **SC-014**: Articles that previously failed with a 429 rate-limit error are successfully extracted via fallback without requiring the caller to retry. [Source: specs/003-medium-freedium-fallback]
278
294
  - **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]
@@ -281,4 +297,4 @@ caller provides URL string
281
297
 
282
298
  ---
283
299
 
284
- *Last Updated: 2026-05-15 | Sources appended: [specs/003-medium-freedium-fallback/spec.md]*
300
+ *Last Updated: 2026-05-15 | Sources appended: [specs/004-remove-backoff/spec.md]*
@@ -51,5 +51,7 @@ uv run mypy src/ # type check
51
51
  ```
52
52
 
53
53
  <!-- SPECKIT START -->
54
- **Recent changes**: `003-medium-freedium-fallback` — Transparent Freedium mirror fallback for Medium 403/429 responses; `_no_retry_status_codes` hook on `BaseExtractor`; `_parse_freedium()` with h4→h3 heading remap on `MediumExtractor`
54
+ ## Recent Changes
55
+
56
+ - **004-remove-backoff**: Removed exponential backoff from `fetch_html`; restored fixed-delay retries (`retry_delay` seconds per attempt, constant between attempts). Deleted `MDFETCH_RETRIES`/`MDFETCH_RETRY_DELAY` env-var support from CI and integration fixtures; hardcoded `retries=3, retry_delay=2.0` in all integration tests. Deleted `tests/integration/conftest.py`.
55
57
  <!-- SPECKIT END -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdfetch
3
- Version: 0.2.1
3
+ Version: 0.2.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mdfetch"
7
- version = "0.2.1"
7
+ version = "0.2.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,34 @@
1
+ # Specification Quality Checklist: Remove Exponential Backoff and Env-Var Retry Config
2
+
3
+ **Purpose**: Validate specification completeness and quality before proceeding to planning
4
+ **Created**: 2026-05-15
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 checklist items pass. Specification is ready for `/speckit-clarify` or `/speckit-plan`.
@@ -0,0 +1,146 @@
1
+ # Implementation Plan: Remove Exponential Backoff and Env-Var Retry Config
2
+
3
+ **Branch**: `004-remove-backoff` | **Date**: 2026-05-15 | **Spec**: [spec.md](spec.md)
4
+
5
+ **Input**: Feature specification from `specs/004-remove-backoff/spec.md`
6
+
7
+ ## Summary
8
+
9
+ Replace the exponential backoff formula introduced in PR #6 (`min(60, retry_delay × 2ⁿ)`) with a simple fixed delay (`retry_delay` seconds, unchanged between attempts). Remove the `_MAX_RETRY_DELAY` module constant, remove `MDFETCH_RETRIES`/`MDFETCH_RETRY_DELAY` env-var reads from the integration test fixtures and CI workflow, and delete the two unit tests that verified the exponential schedule. The `extract()` public API signature and exception contract are entirely unchanged.
10
+
11
+ ## Technical Context
12
+
13
+ **Language/Version**: Python 3.12+
14
+
15
+ **Primary Dependencies**:
16
+
17
+ | Package | Role |
18
+ |---------|------|
19
+ | `httpx` | HTTP client |
20
+ | `beautifulsoup4` / `lxml` | HTML parsing |
21
+ | `markdownify` | Markdown conversion |
22
+ | `pytest` | Test runner |
23
+ | `ruff` / `mypy` | Lint, format, type-check |
24
+
25
+ **Storage**: N/A
26
+
27
+ **Testing**: pytest; unit tests for retry logic; integration tests against real URLs
28
+
29
+ **Target Platform**: Cross-platform PyPI library (Linux, macOS, Windows); Python 3.12+
30
+
31
+ **Project Type**: library
32
+
33
+ **Performance Goals**: N/A — this change only affects retry sleep duration
34
+
35
+ **Constraints**: All 65 existing unit tests and 6 integration tests must pass; `mypy --strict` must pass
36
+
37
+ **Scale/Scope**: Small, targeted removal — 7 files touched, 2 tests deleted, no new files
38
+
39
+ ## Constitution Check
40
+
41
+ *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
42
+
43
+ - [X] Validates Provider Pattern Architecture — no changes to `BaseExtractor` ABC or provider pattern
44
+ - [X] Confirms Technology Stack — no dependency additions or removals
45
+ - [X] Adheres to Coding Standards — PEP 8, strict type hints; removing code only improves compliance
46
+ - [X] Incorporates Integration Testing — integration tests remain; fixtures simplified not removed
47
+ - [X] Respects Packaging and Distribution standards — `pyproject.toml`, `src/` layout, `uv` unchanged
48
+
49
+ **No violations.** Complexity Tracking table not required.
50
+
51
+ ## Project Structure
52
+
53
+ ### Documentation (this feature)
54
+
55
+ ```text
56
+ specs/004-remove-backoff/
57
+ ├── plan.md ← this file
58
+ ├── research.md ← Phase 0 output
59
+ └── tasks.md ← Phase 2 output (/speckit-tasks)
60
+ ```
61
+
62
+ ### Source Code (files changed by this feature)
63
+
64
+ ```text
65
+ src/mdfetch/
66
+ ├── base.py # remove _MAX_RETRY_DELAY; fix sleep formula; update docstring
67
+ └── __init__.py # update docstring (remove exponential reference)
68
+
69
+ tests/
70
+ ├── unit/test_fetch_errors.py # remove 2 backoff tests; update 2 stale "backoff" docstrings; add sleep-value assertion to test_status_code_not_in_no_retry_set_still_retries
71
+ └── integration/
72
+ ├── conftest.py # remove os import + env var reads; simplify/remove fixtures
73
+ ├── test_medium_integration.py # remove fixture params; inline retries=3, retry_delay=2.0
74
+ └── test_devto_integration.py # remove fixture params; inline retries=3, retry_delay=2.0
75
+
76
+ .github/workflows/integration.yml # remove MDFETCH_RETRIES and MDFETCH_RETRY_DELAY env vars
77
+ ```
78
+
79
+ ## Phase 0: Research
80
+
81
+ No NEEDS CLARIFICATION items and no new external dependencies. All design decisions are determined by the existing codebase.
82
+
83
+ See [research.md](research.md) for the decision log.
84
+
85
+ ## Phase 1: Design
86
+
87
+ ### Data Model
88
+
89
+ No entities. This feature removes code; no new data structures are introduced.
90
+
91
+ ### Contracts
92
+
93
+ The public `extract()` signature and exception contract are **unchanged**:
94
+
95
+ ```python
96
+ def extract(url: str, *, retries: int = 3, retry_delay: float = 2.0) -> str
97
+ ```
98
+
99
+ - Same parameters, same defaults, same return type, same exception hierarchy.
100
+ - The only observable behaviour change: the sleep between retries is now a flat `retry_delay` seconds rather than `retry_delay × 2ⁿ`. This is not part of the public API contract.
101
+
102
+ No `contracts/` directory needed.
103
+
104
+ ### Integration Test Strategy
105
+
106
+ **Before** (PR #6 pattern):
107
+
108
+ ```python
109
+ # conftest.py reads env vars
110
+ @pytest.fixture
111
+ def http_retries() -> int:
112
+ return int(os.environ.get("MDFETCH_RETRIES", "3"))
113
+
114
+ # test uses fixture params
115
+ def test_extract_contains_snapshot(
116
+ url: str, snapshot: str, http_retries: int, http_retry_delay: float
117
+ ) -> None:
118
+ result = extract(url, retries=http_retries, retry_delay=http_retry_delay)
119
+ ```
120
+
121
+ **After** (this feature):
122
+
123
+ ```python
124
+ # conftest.py deleted or left empty
125
+ # test uses hardcoded constants
126
+ def test_extract_contains_snapshot(url: str, snapshot: str) -> None:
127
+ result = extract(url, retries=3, retry_delay=2.0)
128
+ ```
129
+
130
+ Rationale: the fixtures existed solely to expose env-var configurability. Without that purpose they are dead abstraction. Inlining `3` and `2.0` is simpler and leaves no misleading indirection.
131
+
132
+ ### Key Changes by File
133
+
134
+ | File | Change |
135
+ |------|--------|
136
+ | `src/mdfetch/base.py` | Remove `_MAX_RETRY_DELAY = 60.0`; change `time.sleep(min(_MAX_RETRY_DELAY, retry_delay * (2**attempt)))` → `time.sleep(retry_delay)`; update docstring |
137
+ | `src/mdfetch/__init__.py` | Update docstring: replace "exponential backoff starting at *retry_delay* seconds" |
138
+ | `tests/unit/test_fetch_errors.py` | Delete `test_exponential_backoff_sleep_sequence` and `test_exponential_backoff_capped_at_max_delay`; update 2 stale "backoff" docstrings; add sleep-value assertion to `test_status_code_not_in_no_retry_set_still_retries` |
139
+ | `tests/integration/conftest.py` | Remove `import os`, remove both fixtures (or delete file if empty) |
140
+ | `tests/integration/test_medium_integration.py` | Remove `http_retries`/`http_retry_delay` fixture params; inline `retries=3, retry_delay=2.0` |
141
+ | `tests/integration/test_devto_integration.py` | Same as above |
142
+ | `.github/workflows/integration.yml` | Remove `MDFETCH_RETRIES: "6"` and `MDFETCH_RETRY_DELAY: "2.0"` from `env:` block |
143
+
144
+ ### Revision: Implementation Sync 2026-05-15
145
+ - Two stale "backoff" docstrings in `tests/unit/test_fetch_errors.py` were not captured in the original plan; added to key-changes table.
146
+ - `test_status_code_not_in_no_retry_set_still_retries` lacked a sleep-value assertion; added to key-changes table to close the AC1 gap.
@@ -0,0 +1,42 @@
1
+ # Research: Remove Exponential Backoff and Env-Var Retry Config
2
+
3
+ **Feature**: `004-remove-backoff` | **Date**: 2026-05-15
4
+
5
+ ## Summary
6
+
7
+ No external research required. All decisions are determined by the existing codebase and the motivation documented in the spec.
8
+
9
+ ---
10
+
11
+ ## Decision 1: Fixed vs Exponential Delay
12
+
13
+ **Decision**: Replace exponential backoff with a fixed `retry_delay` seconds between attempts.
14
+
15
+ **Rationale**: The exponential schedule was introduced to work around aggressive rate-limiting from medium.com in CI. That problem is now solved at a higher level by the Freedium fallback (PR #7): 403/429 responses trigger an immediate switch to Freedium rather than retrying against medium.com at all. The exponential schedule therefore provides no additional resilience but adds complexity and unpredictability to retry timing.
16
+
17
+ **Alternatives considered**:
18
+ - Keep exponential, remove only env vars → Rejected. Removes the tooling noise but keeps the unnecessary complexity in the core retry loop.
19
+ - Keep exponential, add deprecation warning → Rejected. A deprecation period is unnecessary for an internal mechanism not part of the public API.
20
+
21
+ ---
22
+
23
+ ## Decision 2: Remove Fixtures vs Hardcode Defaults
24
+
25
+ **Decision**: Delete the `http_retries` and `http_retry_delay` pytest fixtures from `conftest.py` and inline `retries=3, retry_delay=2.0` directly in integration test calls.
26
+
27
+ **Rationale**: The fixtures existed as thin wrappers around env-var reads. Once the env vars are removed, the fixtures become single-line constants with no abstraction value. Inlining the defaults removes a layer of indirection that would otherwise require readers to trace through `conftest.py` to understand what values are used.
28
+
29
+ **Alternatives considered**:
30
+ - Keep fixtures with hardcoded defaults → Rejected. A fixture returning a literal integer is dead abstraction; it signals configurability that doesn't exist.
31
+ - Move constants to a `conftest.py` module-level variable → Rejected. Same problem — adds indirection without benefit.
32
+
33
+ ---
34
+
35
+ ## Decision 3: Handling `conftest.py` After Fixture Removal
36
+
37
+ **Decision**: Delete `conftest.py` entirely since removing both fixtures leaves the file empty (only `import os` and two functions, all of which are removed).
38
+
39
+ **Rationale**: An empty `conftest.py` is harmless but misleading — it implies there is shared test configuration when there is none. Deleting it is cleaner.
40
+
41
+ **Alternatives considered**:
42
+ - Leave empty file → Rejected. No benefit; creates noise in the file tree.
@@ -0,0 +1,92 @@
1
+ # Feature Specification: Remove Exponential Backoff and Env-Var Retry Config
2
+
3
+ **Feature Branch**: `004-remove-backoff`
4
+
5
+ **Created**: 2026-05-15
6
+
7
+ **Status**: Completed
8
+
9
+ **Input**: User description: "let's deprecate and remove the exponential backoff and env.variables which was implemented in https://github.com/stn1slv/md-fetch/pull/6"
10
+
11
+ ## Background
12
+
13
+ PR #6 introduced two related changes to mdfetch:
14
+
15
+ 1. **Exponential backoff** — the wait before retry attempt *n* was changed from a fixed `retry_delay` seconds to `min(60, retry_delay × 2ⁿ)`. A `_MAX_RETRY_DELAY = 60.0` module-level constant was added to cap the delay.
16
+ 2. **Configurable retries via environment variables** — `tests/integration/conftest.py` reads `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` env vars and exposes them as pytest fixtures. CI sets `MDFETCH_RETRIES=6` in `integration.yml`.
17
+
18
+ Both changes are to be removed, restoring the simpler fixed-delay retry behaviour.
19
+
20
+ ## User Scenarios & Testing *(mandatory)*
21
+
22
+ ### User Story 1 - Fixed-Delay Retry Behaviour Restored (Priority: P1)
23
+
24
+ A developer calling `extract()` encounters a transient network error. The library retries using a simple, predictable fixed delay — exactly `retry_delay` seconds between every attempt — rather than an exponentially growing delay.
25
+
26
+ **Why this priority**: This is the core change. Simple, deterministic retry behaviour is easier to reason about and test. The exponential schedule was added to work around aggressive CI rate-limiting; that problem is now solved at the infrastructure level (Freedium fallback for 403/429) rather than in the retry loop.
27
+
28
+ **Independent Test**: Can be fully tested by calling `fetch_html` with a failing URL and verifying that `time.sleep` is called with the exact `retry_delay` value for every inter-attempt gap (not an exponentially growing value).
29
+
30
+ **Acceptance Scenarios**:
31
+
32
+ 1. **Given** `fetch_html` is called with `retries=3` and `retry_delay=2.0`, **When** the first two attempts raise a transient error, **Then** the library sleeps exactly `2.0` seconds before each retry (not `2.0` then `4.0`).
33
+ 2. **Given** `fetch_html` is called with `retries=1`, **When** the attempt fails, **Then** no sleep occurs and the exception is raised immediately.
34
+ 3. **Given** a status code that is in `_no_retry_status_codes`, **When** that error is raised, **Then** no sleep or retry occurs.
35
+
36
+ ---
37
+
38
+ ### User Story 2 - Integration Tests Use Hardcoded Retry Defaults (Priority: P2)
39
+
40
+ A developer runs `make integration` locally or in CI without setting any `MDFETCH_*` environment variables. The integration tests use fixed defaults (3 retries, 2.0 s delay) and do not consult environment variables.
41
+
42
+ **Why this priority**: Env vars coupling test behaviour to CI configuration makes tests harder to reason about and reproduce locally. With the Freedium fallback absorbing 403/429 errors, 3 retries at a fixed 2 s delay is sufficient.
43
+
44
+ **Independent Test**: Can be fully tested by running `make integration` without setting `MDFETCH_RETRIES` or `MDFETCH_RETRY_DELAY` and verifying all 6 tests pass.
45
+
46
+ **Acceptance Scenarios**:
47
+
48
+ 1. **Given** neither `MDFETCH_RETRIES` nor `MDFETCH_RETRY_DELAY` is set, **When** `make integration` runs, **Then** all integration tests pass using 3 retries and 2.0 s delay.
49
+ 2. **Given** the `MDFETCH_RETRIES=6` env var is set in `integration.yml`, **When** this feature is shipped, **Then** that env var is removed from the CI workflow.
50
+
51
+ ---
52
+
53
+ ### Edge Cases
54
+
55
+ - What happens if tests currently rely on the `http_retries` / `http_retry_delay` fixtures by name? They must be updated to use hardcoded defaults or inline constants.
56
+ - What happens to the two unit tests that assert exponential sleep sequences? They must be removed entirely.
57
+ - What happens to the `_MAX_RETRY_DELAY` module-level constant? It is unused after the change and must be removed to avoid dead code.
58
+
59
+ ## Requirements *(mandatory)*
60
+
61
+ ### Functional Requirements
62
+
63
+ - **FR-001**: The `fetch_html` method MUST use a fixed delay of exactly `retry_delay` seconds between retry attempts — no exponential multiplication.
64
+ - **FR-002**: The `_MAX_RETRY_DELAY` module-level constant in `base.py` MUST be removed.
65
+ - **FR-003**: The `import time` statement in `base.py` MUST be retained (still needed for `time.sleep`).
66
+ - **FR-004**: The `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` environment variable reads in `tests/integration/conftest.py` MUST be removed.
67
+ - **FR-005**: The `http_retries` and `http_retry_delay` pytest fixtures MUST be simplified to return hardcoded defaults (`3` and `2.0` respectively) or be inlined into the test functions.
68
+ - **FR-006**: The `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` env var entries in `.github/workflows/integration.yml` MUST be removed.
69
+ - **FR-007**: The two unit tests `test_exponential_backoff_sleep_sequence` and `test_exponential_backoff_capped_at_max_delay` in `tests/unit/test_fetch_errors.py` MUST be removed.
70
+ - **FR-008**: The `fetch_html` docstring in `base.py` MUST be updated to describe fixed-delay retry behaviour (remove references to exponential schedule and `min(60, …)` formula).
71
+ - **FR-009**: The `extract()` docstring in `src/mdfetch/__init__.py` MUST be updated if it references exponential backoff.
72
+ - **FR-010**: All remaining unit and integration tests MUST continue to pass after the changes.
73
+
74
+ ## Success Criteria *(mandatory)*
75
+
76
+ ### Measurable Outcomes
77
+
78
+ - **SC-001**: `make test` passes with zero failures after the changes (all unit tests green).
79
+ - **SC-002**: `make integration` passes with zero failures when run without any `MDFETCH_*` environment variables.
80
+ - **SC-003**: The two exponential-backoff unit tests no longer exist in the test suite.
81
+ - **SC-004**: No reference to `MDFETCH_RETRIES`, `MDFETCH_RETRY_DELAY`, or `_MAX_RETRY_DELAY` remains in `src/`, `tests/`, or `.github/` (spec and documentation files are excluded).
82
+ - **SC-005**: The CI `integration.yml` workflow no longer sets `MDFETCH_RETRIES` or `MDFETCH_RETRY_DELAY`.
83
+
84
+ ## Assumptions
85
+
86
+ - The Freedium fallback (introduced in PR #7) adequately handles the Medium 403/429 rate-limiting that originally motivated exponential backoff. Fixed delay at 3 retries is therefore sufficient for CI reliability.
87
+ - The `http_retries` and `http_retry_delay` fixtures in `conftest.py` are used only in `test_medium_integration.py` and `test_devto_integration.py`; no other test files depend on them.
88
+ - The `timeout-minutes: 30` setting in `integration.yml` can remain as-is — it provides adequate headroom even with fixed delay retries.
89
+
90
+ ### Revision: Implementation Sync 2026-05-15
91
+ - Stale "backoff" terminology found in two docstrings in `tests/unit/test_fetch_errors.py` (lines 98, 123); these are test comments not covered by FR-004 or FR-007 and require a targeted cleanup task.
92
+ - `test_status_code_not_in_no_retry_set_still_retries` verifies sleep count but not sleep value; US1 AC1 ("sleeps exactly `retry_delay` seconds") implies a value assertion is also needed.
@@ -0,0 +1,147 @@
1
+ # Tasks: Remove Exponential Backoff and Env-Var Retry Config
2
+
3
+ **Input**: Design documents from `specs/004-remove-backoff/`
4
+
5
+ **Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅
6
+
7
+ **Organization**: Tasks are grouped by user story to enable independent implementation and testing.
8
+
9
+ ## Format: `[ID] [P?] [Story] Description`
10
+
11
+ - **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks)
12
+ - **[Story]**: Which user story this task belongs to (US1, US2)
13
+
14
+ ---
15
+
16
+ ## Phase 1: Setup
17
+
18
+ **Purpose**: Establish green baseline before making any changes
19
+
20
+ - [X] T001 Verify baseline by running `make test` — confirm 65 unit tests pass before changes
21
+
22
+ ---
23
+
24
+ ## Phase 2: Foundational (Blocking Prerequisites)
25
+
26
+ **Purpose**: No shared infrastructure changes needed for this feature — this phase is satisfied by T001.
27
+
28
+ **⚠️ NOTE**: US1 and US2 are fully independent; either can be implemented first or in parallel.
29
+
30
+ ---
31
+
32
+ ## Phase 3: User Story 1 — Fixed-Delay Retry Behaviour Restored (Priority: P1) 🎯 MVP
33
+
34
+ **Goal**: Replace the exponential backoff formula with a flat `retry_delay` seconds sleep; remove the `_MAX_RETRY_DELAY` cap constant; update docstrings; delete the two unit tests that verified the exponential schedule.
35
+
36
+ **Independent Test**: Run `make test` — the remaining retry tests (`TestNoRetryStatusCodes`, `TestFetchErrors`) must pass; the two deleted tests must no longer exist.
37
+
38
+ ### Implementation for User Story 1
39
+
40
+ - [X] T002 [US1] Remove `_MAX_RETRY_DELAY = 60.0` constant and change `time.sleep(min(_MAX_RETRY_DELAY, retry_delay * (2**attempt)))` to `time.sleep(retry_delay)` in `src/mdfetch/base.py`
41
+ - [X] T003 [US1] Update `fetch_html` docstring in `src/mdfetch/base.py` to say "fixed delay of *retry_delay* seconds" (remove mention of exponential schedule and `min(60, …)` formula)
42
+ - [X] T004 [P] [US1] Update `extract()` docstring in `src/mdfetch/__init__.py` — replace "exponential backoff starting at *retry_delay* seconds" with "fixed delay of *retry_delay* seconds between attempts"
43
+ - [X] T005 [P] [US1] Delete `test_exponential_backoff_sleep_sequence` and `test_exponential_backoff_capped_at_max_delay` test methods from `tests/unit/test_fetch_errors.py`
44
+
45
+ **Checkpoint**: Run `make test` — all remaining unit tests pass; no reference to exponential backoff remains in `src/`.
46
+
47
+ ---
48
+
49
+ ## Phase 4: User Story 2 — Integration Tests Use Hardcoded Retry Defaults (Priority: P2)
50
+
51
+ **Goal**: Remove the `MDFETCH_RETRIES` / `MDFETCH_RETRY_DELAY` env-var fixtures from the integration test suite and CI; inline the defaults directly in test call sites; delete `conftest.py`.
52
+
53
+ **Independent Test**: Run `make integration` without any `MDFETCH_*` env vars set — all 6 integration tests pass.
54
+
55
+ ### Implementation for User Story 2
56
+
57
+ - [X] T006 [P] [US2] Remove `http_retries` and `http_retry_delay` fixture parameters from the test function signature and inline `retries=3, retry_delay=2.0` in the `extract()` call in `tests/integration/test_medium_integration.py`; remove the env-var docstring line from the test docstring
58
+ - [X] T007 [P] [US2] Same changes as T006 in `tests/integration/test_devto_integration.py`
59
+ - [X] T008 [US2] Delete `tests/integration/conftest.py` (file is empty after T006 + T007 remove all fixture consumers; no other test files reference these fixtures)
60
+ - [X] T009 [US2] Remove the `MDFETCH_RETRIES: "6"` and `MDFETCH_RETRY_DELAY: "2.0"` lines from the `env:` block of the `Run integration tests` step in `.github/workflows/integration.yml`
61
+
62
+ **Checkpoint**: Run `make integration` — all 6 tests pass; no `MDFETCH_*` variables appear anywhere in the codebase.
63
+
64
+ ---
65
+
66
+ ## Phase 5: Polish & Cross-Cutting Concerns
67
+
68
+ **Purpose**: Final validation — confirm all quality gates pass together
69
+
70
+ - [X] T010 Run `make test` to confirm all unit tests pass (expected: 63 tests — 65 minus the 2 deleted backoff tests)
71
+ - [X] T011 Run `uv run mypy src/` to confirm zero type errors after docstring and code changes
72
+ - [X] T012 Run `make lint` to confirm ruff check passes with no violations
73
+ - [X] T013 Verify no remaining references to `_MAX_RETRY_DELAY`, `MDFETCH_RETRIES`, or `MDFETCH_RETRY_DELAY` in source, tests, and CI by running `grep -r "MAX_RETRY_DELAY\|MDFETCH_RETRIES\|MDFETCH_RETRY_DELAY" src/ tests/ .github/` (spec/doc files are intentionally excluded)
74
+ - [X] T014 Run `make integration` without any `MDFETCH_*` env vars set and confirm all 6 integration tests pass (verifies SC-002)
75
+
76
+ ---
77
+
78
+ ## Remediation: Gaps
79
+
80
+ *Added by Implementation Sync 2026-05-15*
81
+
82
+ - [X] T015 [P] [US1] Update stale "backoff" docstrings in `tests/unit/test_fetch_errors.py`: line 98 "no backoff sleep" → "no retry sleep"; line 123 "trigger backoff" → "trigger retry with fixed delay" [Sync: Gap Report]
83
+ - [X] T016 [US1] Add sleep-value assertion to `test_status_code_not_in_no_retry_set_still_retries` in `tests/unit/test_fetch_errors.py` — add `assert mock_sleep.call_args_list == [call(1.0), call(1.0)]` after the call_count check (verifies US1 AC1: "sleeps exactly retry_delay seconds") [Sync: Gap Report]
84
+
85
+ ---
86
+
87
+ ## Dependencies & Execution Order
88
+
89
+ ### Phase Dependencies
90
+
91
+ - **Setup (Phase 1)**: No dependencies — start immediately
92
+ - **US1 (Phase 3)**: Depends on T001 (baseline green) — can start after setup
93
+ - **US2 (Phase 4)**: Depends on T001 (baseline green) — **independent from US1; can run in parallel**
94
+ - **Polish (Phase 5)**: Depends on US1 and US2 both complete
95
+
96
+ ### Within Each User Story
97
+
98
+ - **US1**: T002 → T003 (same file, sequential); T004 and T005 are [P] (different files, independent of T002/T003)
99
+ - **US2**: T006 and T007 are [P] (different files); T008 depends on T006 + T007 (delete conftest only after test files no longer reference fixtures); T009 is [P] with T006/T007/T008 (different file)
100
+
101
+ ### Parallel Opportunities
102
+
103
+ ```text
104
+ After T001 (baseline green):
105
+
106
+ Stream A (US1):
107
+ T002 → T003 # base.py: formula + docstring (same file, sequential)
108
+ T004 # __init__.py docstring [P with T005]
109
+ T005 # delete backoff unit tests [P with T004]
110
+
111
+ Stream B (US2):
112
+ T006 [P] # test_medium_integration.py (parallel with T007)
113
+ T007 [P] # test_devto_integration.py (parallel with T006)
114
+ T008 # delete conftest.py (after T006 + T007)
115
+ T009 # integration.yml env vars [P, independent]
116
+
117
+ After both streams: T010 → T011 → T012 → T013 → T014
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Implementation Strategy
123
+
124
+ ### MVP First (User Story 1 Only)
125
+
126
+ 1. T001 — establish baseline
127
+ 2. T002 → T003 — fix retry formula and docstring in `base.py`
128
+ 3. T004 + T005 — update `__init__.py` docstring; delete backoff tests
129
+ 4. T010 + T011 + T012 — validate
130
+ 5. **STOP and VALIDATE**: `make test` passes with 63 tests
131
+
132
+ ### Full Delivery (US1 + US2)
133
+
134
+ Continue after MVP:
135
+ 6. T006 + T007 — update integration test signatures
136
+ 7. T008 — delete conftest.py
137
+ 8. T009 — remove CI env vars
138
+ 9. T013 — confirm no residual references
139
+
140
+ ---
141
+
142
+ ## Notes
143
+
144
+ - [P] tasks operate on different files with no shared dependencies — safe to run concurrently
145
+ - T008 (delete conftest.py) MUST come after T006 + T007 — deleting conftest.py while test functions still declare `http_retries`/`http_retry_delay` parameters will cause pytest fixture-not-found errors
146
+ - T009 (.github/workflows) is independent of all other tasks; can be done at any point after T001
147
+ - Expected final unit test count: 63 (65 baseline − 2 deleted backoff tests)