mdfetch 0.2.2__tar.gz → 0.4.0__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.
- mdfetch-0.4.0/.github/copilot-instructions.md +1 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/git-config.yml +17 -17
- mdfetch-0.4.0/.specify/feature.json +3 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/memory/changelog.md +55 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/memory/plan.md +63 -13
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/memory/spec.md +139 -5
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/templates/checklist-template.md +1 -0
- mdfetch-0.4.0/.specify/templates/constitution-template.md +44 -0
- mdfetch-0.4.0/.specify/templates/plan-template.md +128 -0
- mdfetch-0.4.0/.specify/templates/spec-template.md +151 -0
- mdfetch-0.4.0/.specify/templates/tasks-template.md +220 -0
- mdfetch-0.4.0/CLAUDE.md +76 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/PKG-INFO +8 -2
- {mdfetch-0.2.2 → mdfetch-0.4.0}/README.md +6 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/pyproject.toml +2 -2
- mdfetch-0.4.0/specs/005-substack-provider/checklists/requirements.md +36 -0
- mdfetch-0.4.0/specs/005-substack-provider/contracts/extractor-api.md +59 -0
- mdfetch-0.4.0/specs/005-substack-provider/data-model.md +34 -0
- mdfetch-0.4.0/specs/005-substack-provider/plan.md +136 -0
- mdfetch-0.4.0/specs/005-substack-provider/quickstart.md +44 -0
- mdfetch-0.4.0/specs/005-substack-provider/research.md +75 -0
- mdfetch-0.4.0/specs/005-substack-provider/spec.md +117 -0
- mdfetch-0.4.0/specs/005-substack-provider/tasks.md +209 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/checklists/requirements.md +37 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/contracts/public-api.md +54 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/data-model.md +60 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/plan.md +139 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/quickstart.md +81 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/research.md +79 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/spec.md +101 -0
- mdfetch-0.4.0/specs/006-thenewstack-provider/tasks.md +224 -0
- mdfetch-0.4.0/specs/007-dzone-provider/checklists/requirements.md +37 -0
- mdfetch-0.4.0/specs/007-dzone-provider/contracts/public-api.md +53 -0
- mdfetch-0.4.0/specs/007-dzone-provider/data-model.md +70 -0
- mdfetch-0.4.0/specs/007-dzone-provider/plan.md +136 -0
- mdfetch-0.4.0/specs/007-dzone-provider/quickstart.md +107 -0
- mdfetch-0.4.0/specs/007-dzone-provider/research.md +85 -0
- mdfetch-0.4.0/specs/007-dzone-provider/spec.md +118 -0
- mdfetch-0.4.0/specs/007-dzone-provider/tasks.md +272 -0
- mdfetch-0.4.0/src/mdfetch/providers/dzone.py +88 -0
- mdfetch-0.4.0/src/mdfetch/providers/substack.py +86 -0
- mdfetch-0.4.0/src/mdfetch/providers/thenewstack.py +76 -0
- mdfetch-0.4.0/tests/integration/conftest.py +9 -0
- mdfetch-0.4.0/tests/integration/snapshots/dzone-image-classification-pipeline-camel-djl.md +29 -0
- mdfetch-0.4.0/tests/integration/snapshots/dzone-integration-patterns-fail-production.md +29 -0
- mdfetch-0.4.0/tests/integration/snapshots/dzone-kiro-feature-to-requirements-design-tasks.md +29 -0
- mdfetch-0.4.0/tests/integration/snapshots/substack-api-trends-2025.md +99 -0
- mdfetch-0.4.0/tests/integration/snapshots/substack-kafka-topic-types.md +85 -0
- mdfetch-0.4.0/tests/integration/snapshots/thenewstack-api-mcp-agent.md +29 -0
- mdfetch-0.4.0/tests/integration/snapshots/thenewstack-async-apis.md +29 -0
- mdfetch-0.4.0/tests/integration/snapshots/thenewstack-developer-portal-api.md +29 -0
- mdfetch-0.4.0/tests/integration/snapshots/thenewstack-json-schema-ai.md +29 -0
- mdfetch-0.4.0/tests/integration/snapshots/thenewstack-mcp-api-governance.md +29 -0
- mdfetch-0.4.0/tests/integration/test_dzone_integration.py +56 -0
- mdfetch-0.4.0/tests/integration/test_substack_integration.py +51 -0
- mdfetch-0.4.0/tests/integration/test_thenewstack_integration.py +64 -0
- mdfetch-0.4.0/tests/unit/test_dzone_extractor.py +162 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/unit/test_router.py +15 -3
- mdfetch-0.4.0/tests/unit/test_substack_extractor.py +278 -0
- mdfetch-0.4.0/tests/unit/test_thenewstack_extractor.py +207 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/uv.lock +52 -52
- mdfetch-0.2.2/.specify/feature.json +0 -3
- mdfetch-0.2.2/.specify/templates/constitution-template.md +0 -50
- mdfetch-0.2.2/.specify/templates/plan-template.md +0 -117
- mdfetch-0.2.2/.specify/templates/spec-template.md +0 -129
- mdfetch-0.2.2/.specify/templates/tasks-template.md +0 -252
- mdfetch-0.2.2/CLAUDE.md +0 -57
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-analyze/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-archive-run/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-checklist/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-clarify/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-constitution/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-implement/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-plan/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-reconcile-run/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-specify/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-tasks/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.analyze.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.archive.run.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.checklist.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.clarify.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.constitution.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.implement.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.plan.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.reconcile.run.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.specify.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.tasks.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gemini/commands/speckit.taskstoissues.toml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.github/workflows/ci.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.github/workflows/integration.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.github/workflows/publish.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.gitignore +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.python-version +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/.registry +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/archive/LICENSE +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/archive/README.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/archive/commands/archive.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/archive/extension.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/README.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/config-template.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/extension.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/reconcile/LICENSE +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/reconcile/README.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/reconcile/commands/reconcile.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions/reconcile/extension.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/extensions.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/init-options.json +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/integration.json +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/integrations/claude.manifest.json +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/integrations/gemini.manifest.json +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/integrations/speckit.manifest.json +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/memory/constitution.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/scripts/bash/common.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/scripts/bash/setup-plan.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/scripts/bash/setup-tasks.sh +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/workflows/speckit/workflow.yml +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/.specify/workflows/workflow-registry.json +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/GEMINI.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/LICENSE +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/Makefile +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/checklists/requirements.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/contracts/api.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/data-model.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/plan.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/quickstart.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/research.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/spec.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/001-mdfetch-medium-extractor/tasks.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/checklists/requirements.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/contracts/public-api.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/data-model.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/plan.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/quickstart.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/research.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/spec.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/002-devto-provider/tasks.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/003-medium-freedium-fallback/checklists/requirements.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/003-medium-freedium-fallback/contracts/extract-api.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/003-medium-freedium-fallback/plan.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/003-medium-freedium-fallback/research.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/003-medium-freedium-fallback/spec.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/003-medium-freedium-fallback/tasks.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/004-remove-backoff/checklists/requirements.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/004-remove-backoff/plan.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/004-remove-backoff/research.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/004-remove-backoff/spec.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/specs/004-remove-backoff/tasks.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/src/mdfetch/__init__.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/src/mdfetch/base.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/src/mdfetch/exceptions.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/src/mdfetch/providers/__init__.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/src/mdfetch/providers/devto.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/src/mdfetch/providers/medium.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/src/mdfetch/router.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/__init__.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/conftest.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/__init__.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/snapshots/architecting-the-asynchronous-agent.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/snapshots/devto-integration-digest-december-2025.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/snapshots/devto-integration-digest-july-2025.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/snapshots/devto-integration-digest-march-2026.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/snapshots/from-drift-to-parity.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/snapshots/integration-digest-december-2025.md +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/test_devto_integration.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/integration/test_medium_integration.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/unit/__init__.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/unit/test_devto_extractor.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/unit/test_fetch_errors.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/unit/test_medium_extractor.py +0 -0
- {mdfetch-0.2.2 → mdfetch-0.4.0}/tests/unit/test_silent.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../CLAUDE.md
|
|
@@ -11,52 +11,52 @@ init_commit_message: "[Spec Kit] Initial commit"
|
|
|
11
11
|
# Set "default" to enable for all commands, then override per-command.
|
|
12
12
|
# Each key can be true/false. Message is customizable per-command.
|
|
13
13
|
auto_commit:
|
|
14
|
-
default:
|
|
14
|
+
default: true
|
|
15
15
|
before_clarify:
|
|
16
|
-
enabled:
|
|
16
|
+
enabled: true
|
|
17
17
|
message: "[Spec Kit] Save progress before clarification"
|
|
18
18
|
before_plan:
|
|
19
|
-
enabled:
|
|
19
|
+
enabled: true
|
|
20
20
|
message: "[Spec Kit] Save progress before planning"
|
|
21
21
|
before_tasks:
|
|
22
|
-
enabled:
|
|
22
|
+
enabled: true
|
|
23
23
|
message: "[Spec Kit] Save progress before task generation"
|
|
24
24
|
before_implement:
|
|
25
|
-
enabled:
|
|
25
|
+
enabled: true
|
|
26
26
|
message: "[Spec Kit] Save progress before implementation"
|
|
27
27
|
before_checklist:
|
|
28
|
-
enabled:
|
|
28
|
+
enabled: true
|
|
29
29
|
message: "[Spec Kit] Save progress before checklist"
|
|
30
30
|
before_analyze:
|
|
31
|
-
enabled:
|
|
31
|
+
enabled: true
|
|
32
32
|
message: "[Spec Kit] Save progress before analysis"
|
|
33
33
|
before_taskstoissues:
|
|
34
|
-
enabled:
|
|
34
|
+
enabled: true
|
|
35
35
|
message: "[Spec Kit] Save progress before issue sync"
|
|
36
36
|
after_constitution:
|
|
37
|
-
enabled:
|
|
37
|
+
enabled: true
|
|
38
38
|
message: "[Spec Kit] Add project constitution"
|
|
39
39
|
after_specify:
|
|
40
|
-
enabled:
|
|
40
|
+
enabled: true
|
|
41
41
|
message: "[Spec Kit] Add specification"
|
|
42
42
|
after_clarify:
|
|
43
|
-
enabled:
|
|
43
|
+
enabled: true
|
|
44
44
|
message: "[Spec Kit] Clarify specification"
|
|
45
45
|
after_plan:
|
|
46
|
-
enabled:
|
|
46
|
+
enabled: true
|
|
47
47
|
message: "[Spec Kit] Add implementation plan"
|
|
48
48
|
after_tasks:
|
|
49
|
-
enabled:
|
|
49
|
+
enabled: true
|
|
50
50
|
message: "[Spec Kit] Add tasks"
|
|
51
51
|
after_implement:
|
|
52
|
-
enabled:
|
|
52
|
+
enabled: true
|
|
53
53
|
message: "[Spec Kit] Implementation progress"
|
|
54
54
|
after_checklist:
|
|
55
|
-
enabled:
|
|
55
|
+
enabled: true
|
|
56
56
|
message: "[Spec Kit] Add checklist"
|
|
57
57
|
after_analyze:
|
|
58
|
-
enabled:
|
|
58
|
+
enabled: true
|
|
59
59
|
message: "[Spec Kit] Add analysis report"
|
|
60
60
|
after_taskstoissues:
|
|
61
|
-
enabled:
|
|
61
|
+
enabled: true
|
|
62
62
|
message: "[Spec Kit] Sync tasks to issues"
|
|
@@ -2,6 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
### mdfetch — The New Stack Provider — 2026-05-16
|
|
6
|
+
|
|
7
|
+
**Branch**: `006-thenewstack-provider`
|
|
8
|
+
**Spec**: specs/006-thenewstack-provider
|
|
9
|
+
|
|
10
|
+
**What was added**:
|
|
11
|
+
- `TheNewStackExtractor` provider for `thenewstack.io` articles, auto-discovered via `@register` decorator
|
|
12
|
+
- Article body isolation from `div#tns-post-body-content`; title prepended from `h1.title` in `div#tns-post-headline`; optional deck/subtitle prepended as plain `<p>` tag from `div.post-excerpt`
|
|
13
|
+
- 4 sponsored-content selectors decomposed from body: `div.sponsored-post-disclosure`, `div.tns-sponsored-post-disclosure`, `div.sponsor-disclosure`, `div.tns-sponsor-note`
|
|
14
|
+
- iframes converted to plain anchor links (defensive; not observed in reference articles)
|
|
15
|
+
- `UnsupportedContentTypeError` raised when `div#tns-post-body-content` is absent; `EmptyContentError` raised when body yields no extractable text
|
|
16
|
+
- 14 unit tests in `tests/unit/test_thenewstack_extractor.py`; 6 integration tests (5 article snapshots + 1 homepage error) in `tests/integration/test_thenewstack_integration.py`
|
|
17
|
+
- VoxPop polls (`div.tns-voxpop-screen`) confirmed as page-level modals outside body — no stripping needed
|
|
18
|
+
- Snapshots use verbatim first-30-line prefix format (preserves blank lines for containment assertion)
|
|
19
|
+
|
|
20
|
+
**New Components**:
|
|
21
|
+
- `src/mdfetch/providers/thenewstack.py` — TheNewStackExtractor
|
|
22
|
+
- `tests/unit/test_thenewstack_extractor.py` — 14 unit tests
|
|
23
|
+
- `tests/integration/test_thenewstack_integration.py` — 6 integration tests
|
|
24
|
+
- `tests/integration/snapshots/thenewstack-developer-portal-api.md`
|
|
25
|
+
- `tests/integration/snapshots/thenewstack-async-apis.md`
|
|
26
|
+
- `tests/integration/snapshots/thenewstack-json-schema-ai.md`
|
|
27
|
+
- `tests/integration/snapshots/thenewstack-mcp-api-governance.md`
|
|
28
|
+
- `tests/integration/snapshots/thenewstack-api-mcp-agent.md`
|
|
29
|
+
|
|
30
|
+
**Tasks Completed**: 20/20
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
### mdfetch — Substack Provider — 2026-05-15
|
|
35
|
+
|
|
36
|
+
**Branch**: `005-substack-provider`
|
|
37
|
+
**Spec**: specs/005-substack-provider
|
|
38
|
+
|
|
39
|
+
**What was added**:
|
|
40
|
+
- `SubstackExtractor` provider for `substack.com` and all `*.substack.com` subdomain articles, auto-discovered via `@register` decorator
|
|
41
|
+
- Article body isolation from `div.body.markup`; title prepended from `h1.post-title` in `div.post-header` (unconditional — structurally outside body); optional subtitle from `h3.subtitle` prepended after title
|
|
42
|
+
- `div.subscription-widget-wrap` stripped (inline subscribe CTAs and paywall terminal widget — achieves silent free-preview truncation for paywalled posts without error or marker)
|
|
43
|
+
- `<iframe>` and unknown `div[data-component-name]` embed containers converted to plain anchor links (consistent with dev.to embed handling)
|
|
44
|
+
- HTTP 429 retried via base-class fixed-delay retry (no `_no_retry_status_codes` override — no Freedium-style mirror for Substack)
|
|
45
|
+
- `UnsupportedContentTypeError` raised when `div.body.markup` is absent; `EmptyContentError` raised when body yields no extractable text
|
|
46
|
+
- 18 unit tests in `tests/unit/test_substack_extractor.py`; 3 integration tests in `tests/integration/test_substack_integration.py` with 2 snapshot golden files
|
|
47
|
+
- `test_router.py` unsupported-domain fixture updated from `substack.com` to `wordpress.com`
|
|
48
|
+
|
|
49
|
+
**New Components**:
|
|
50
|
+
- `src/mdfetch/providers/substack.py` — SubstackExtractor
|
|
51
|
+
- `tests/unit/test_substack_extractor.py` — 18 unit tests
|
|
52
|
+
- `tests/integration/test_substack_integration.py` — 3 integration tests
|
|
53
|
+
- `tests/integration/snapshots/substack-kafka-topic-types.md`
|
|
54
|
+
- `tests/integration/snapshots/substack-api-trends-2025.md`
|
|
55
|
+
|
|
56
|
+
**Tasks Completed**: 21/21
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
5
60
|
### mdfetch — Remove Exponential Backoff — 2026-05-15
|
|
6
61
|
|
|
7
62
|
**Branch**: `004-remove-backoff`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# mdfetch — Main Implementation Plan
|
|
2
2
|
|
|
3
|
-
**Last Updated**: 2026-05-
|
|
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]
|
|
3
|
+
**Last Updated**: 2026-05-16
|
|
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], [specs/005-substack-provider/plan.md], [specs/006-thenewstack-provider/plan.md]
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -63,6 +63,29 @@ DevToExtractor(BaseExtractor) — src/mdfetch/providers/devto.py
|
|
|
63
63
|
│ replaces iframes and ltag embeds with anchor links; strips empty anchor-name
|
|
64
64
|
│ elements; prepends h1 + cover image from crayons-article__header
|
|
65
65
|
└── convert_to_markdown() → markdownify with ATX headings; collapses 3+ newlines to 2
|
|
66
|
+
|
|
67
|
+
SubstackExtractor(BaseExtractor) — src/mdfetch/providers/substack.py [005-substack-provider]
|
|
68
|
+
├── DOMAINS = frozenset({"substack.com"}) — also matches *.substack.com via suffix routing
|
|
69
|
+
├── _no_retry_status_codes = frozenset() — HTTP 429 retried (no Freedium-style fallback)
|
|
70
|
+
├── clean_html() → locates div.body.markup; raises UnsupportedContentTypeError if absent;
|
|
71
|
+
│ strips div.subscription-widget-wrap (inline CTAs + paywall terminal);
|
|
72
|
+
│ replaces iframes with anchor links using src/data-src;
|
|
73
|
+
│ replaces div[data-component-name] (except SubscribeWidget, Image2ToDOM) with anchor links;
|
|
74
|
+
│ prepends h3.subtitle from div.post-header (if present);
|
|
75
|
+
│ prepends h1.post-title from div.post-header (unconditional — structurally outside body)
|
|
76
|
+
└── convert_to_markdown() → markdownify with ATX headings; collapses 3+ newlines to 2; raises EmptyContentError if empty
|
|
77
|
+
|
|
78
|
+
TheNewStackExtractor(BaseExtractor) — src/mdfetch/providers/thenewstack.py [006-thenewstack-provider]
|
|
79
|
+
├── DOMAINS = frozenset({"thenewstack.io"}) — no subdomain routing needed (single-site WordPress)
|
|
80
|
+
├── _no_retry_status_codes = frozenset() — inherits base class default (no overrides needed)
|
|
81
|
+
├── clean_html() → locates div#tns-post-body-content; raises UnsupportedContentTypeError if absent;
|
|
82
|
+
│ decomposes 4 sponsored-content selectors: div.sponsored-post-disclosure,
|
|
83
|
+
│ div.tns-sponsored-post-disclosure, div.sponsor-disclosure, div.tns-sponsor-note;
|
|
84
|
+
│ replaces iframes with anchor links using src/data-src (defensive; not observed in reference articles);
|
|
85
|
+
│ prepends div.post-excerpt text as new <p> tag (deck) from div#tns-post-headline (if present);
|
|
86
|
+
│ prepends copy.copy(h1.title) from div#tns-post-headline (if present)
|
|
87
|
+
│ Note: VoxPop polls (div.tns-voxpop-screen) are page-level modals outside body — no stripping needed
|
|
88
|
+
└── convert_to_markdown() → markdownify with ATX headings; collapses 3+ newlines to 2; raises EmptyContentError if empty
|
|
66
89
|
```
|
|
67
90
|
|
|
68
91
|
### Router / Auto-Discovery
|
|
@@ -99,7 +122,9 @@ src/
|
|
|
99
122
|
└── providers/
|
|
100
123
|
├── __init__.py # Empty — auto-discovery handles registration
|
|
101
124
|
├── medium.py # MediumExtractor
|
|
102
|
-
|
|
125
|
+
├── devto.py # DevToExtractor [002-devto-provider]
|
|
126
|
+
├── substack.py # SubstackExtractor [005-substack-provider]
|
|
127
|
+
└── thenewstack.py # TheNewStackExtractor [006-thenewstack-provider]
|
|
103
128
|
|
|
104
129
|
tests/
|
|
105
130
|
├── unit/
|
|
@@ -107,17 +132,28 @@ tests/
|
|
|
107
132
|
│ ├── test_medium_extractor.py
|
|
108
133
|
│ ├── test_fetch_errors.py
|
|
109
134
|
│ ├── test_silent.py
|
|
110
|
-
│
|
|
135
|
+
│ ├── test_devto_extractor.py # [002-devto-provider]
|
|
136
|
+
│ ├── test_substack_extractor.py # [005-substack-provider]
|
|
137
|
+
│ └── test_thenewstack_extractor.py # [006-thenewstack-provider]
|
|
111
138
|
└── integration/
|
|
112
139
|
├── snapshots/ # Golden Markdown files (article body snapshots)
|
|
113
140
|
│ ├── from-drift-to-parity.md
|
|
114
141
|
│ ├── architecting-the-asynchronous-agent.md
|
|
115
142
|
│ ├── integration-digest-december-2025.md
|
|
116
|
-
│ ├── devto-integration-digest-december-2025.md
|
|
117
|
-
│ ├── devto-integration-digest-july-2025.md
|
|
118
|
-
│
|
|
143
|
+
│ ├── devto-integration-digest-december-2025.md # [002-devto-provider]
|
|
144
|
+
│ ├── devto-integration-digest-july-2025.md # [002-devto-provider]
|
|
145
|
+
│ ├── devto-integration-digest-march-2026.md # [002-devto-provider]
|
|
146
|
+
│ ├── substack-kafka-topic-types.md # [005-substack-provider]
|
|
147
|
+
│ ├── substack-api-trends-2025.md # [005-substack-provider]
|
|
148
|
+
│ ├── thenewstack-developer-portal-api.md # [006-thenewstack-provider]
|
|
149
|
+
│ ├── thenewstack-async-apis.md # [006-thenewstack-provider]
|
|
150
|
+
│ ├── thenewstack-json-schema-ai.md # [006-thenewstack-provider]
|
|
151
|
+
│ ├── thenewstack-mcp-api-governance.md # [006-thenewstack-provider]
|
|
152
|
+
│ └── thenewstack-api-mcp-agent.md # [006-thenewstack-provider]
|
|
119
153
|
├── test_medium_integration.py
|
|
120
|
-
|
|
154
|
+
├── test_devto_integration.py # [002-devto-provider]
|
|
155
|
+
├── test_substack_integration.py # [005-substack-provider]
|
|
156
|
+
└── test_thenewstack_integration.py # [006-thenewstack-provider]
|
|
121
157
|
|
|
122
158
|
specs/ # Speckit feature specifications
|
|
123
159
|
pyproject.toml # hatchling build backend, uv package manager
|
|
@@ -140,17 +176,21 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
140
176
|
|
|
141
177
|
## Testing Strategy
|
|
142
178
|
|
|
143
|
-
**Unit tests** (
|
|
179
|
+
**Unit tests** (101 tests, offline):
|
|
144
180
|
- Router: domain routing, subdomain suffix matching, duplicate registration, invalid URLs, unsupported platforms
|
|
145
181
|
- 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]
|
|
146
182
|
- 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]
|
|
183
|
+
- SubstackExtractor: routing (subdomain + root domain + _no_retry_status_codes assertion), clean_html (body.markup tag return, subscription-widget strip, title prepend, subtitle prepend, prose preservation, iframe→anchor), convert_to_markdown (title heading, no triple blank lines, image syntax, link preservation), paywalled post (non-empty, Subscribe text absent, free preview present), error cases (UnsupportedContentTypeError on no body, EmptyContentError on whitespace body) [005-substack-provider]
|
|
184
|
+
- TheNewStackExtractor: routing (thenewstack.io domain), clean_html (body div return, title prepend, deck-as-paragraph prepend, sponsor note strip, all 3 disclosure variant strips, iframe→anchor, no deck when absent), convert_to_markdown (title heading, deck after title, no triple blank lines, image syntax, link preservation), error cases (UnsupportedContentTypeError on no body, EmptyContentError on whitespace body) [006-thenewstack-provider]
|
|
147
185
|
- Fetch errors: HTTP 404, 503, timeout, connection error, size limit exceeded; `_no_retry_status_codes` immediate-raise + `_no_retry_codes` override [003-medium-freedium-fallback]
|
|
148
186
|
- Silent: no stdout/stderr output, no logging during extraction
|
|
149
187
|
|
|
150
|
-
**Integration tests** (
|
|
188
|
+
**Integration tests** (15 tests, network required):
|
|
151
189
|
- 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]
|
|
152
190
|
- Parametrized over 3 real dev.to/stn1slv articles [002-devto-provider]
|
|
153
|
-
-
|
|
191
|
+
- Parametrized over 2 real Substack articles + 1 homepage error test (`UnsupportedContentTypeError`) [005-substack-provider]
|
|
192
|
+
- Parametrized over 5 real thenewstack.io articles + 1 homepage error test (`UnsupportedContentTypeError`); snapshots are verbatim first-30-line prefixes of the full extraction output [006-thenewstack-provider]
|
|
193
|
+
- Snapshot-based containment check: `expected_body in extracted_result` — tests pass regardless of which source served the content
|
|
154
194
|
- 3 retries with 2-second **fixed** delay on `FetchError` (hardcoded; not env-var configurable) [004-remove-backoff]
|
|
155
195
|
- Run with: `make integration` or `uv run pytest tests/integration/ --override-ini=addopts=`
|
|
156
196
|
- Excluded from default `pytest` run via `addopts = "-m 'not integration'"` in pyproject.toml
|
|
@@ -187,6 +227,16 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
187
227
|
| Routing | `pkgutil.iter_modules` auto-discovery + `@register` | SC-006: one new file = one new platform |
|
|
188
228
|
| Integration tests | Snapshot containment + retry | Durable against minor HTML changes; resilient to transient 403s |
|
|
189
229
|
| test_router.py domain example | Changed from `dev.to` to `substack.com` for "unsupported domain" test | Once DevToExtractor registers `dev.to`, those tests would no longer raise UnsupportedPlatformError | [002-devto-provider]
|
|
230
|
+
| test_router.py domain example | Changed from `substack.com` to `wordpress.com` for "unsupported domain" test | Once SubstackExtractor registers `substack.com`, those tests would no longer raise UnsupportedPlatformError | [005-substack-provider]
|
|
231
|
+
| Substack article targeting | `div.body.markup` as extraction root | Contains article prose only; `div.available-content` is a transparent wrapper; `div.post-footer` and `div.visibility-check` are sibling elements never encountered when using body as root | [005-substack-provider]
|
|
232
|
+
| Substack title prepend | Unconditional prepend of `h1.post-title` from `div.post-header` | Structurally guaranteed outside `div.body.markup`; section headings use distinct class `header-anchor-post` — no deduplication needed | [005-substack-provider]
|
|
233
|
+
| Substack subtitle | Prepend `h3.subtitle` after title (inserted at index 0 first, then title at index 0 displaces it to index 1) | Author intent preserved; subtitle rendered as `###` heading | [005-substack-provider]
|
|
234
|
+
| Substack HTTP 429 | No `_no_retry_status_codes` override — base class `frozenset()` applies | Unlike Medium, Substack has no Freedium-style mirror; retry is the correct fallback | [005-substack-provider]
|
|
235
|
+
| thenewstack.io article body | `div#tns-post-body-content` | Innermost element containing only prose (29 direct `<p>` children in reference articles); parent chain includes several wrapper divs that add no content | [006-thenewstack-provider]
|
|
236
|
+
| thenewstack.io deck element | Create new `<p>` tag with deck text rather than copying `div.post-excerpt` directly | `div.post-excerpt` is a `<div>`, not a semantic subtitle; wrapping text in `<p>` ensures proper paragraph rendering in Markdown | [006-thenewstack-provider]
|
|
237
|
+
| thenewstack.io VoxPop polls | No explicit stripping required | `div.tns-voxpop-screen` confirmed absent from `div#tns-post-body-content` across all 5 reference articles — page-level overlay modal, not inline content | [006-thenewstack-provider]
|
|
238
|
+
| thenewstack.io router test | No update to `test_router.py` unsupported-domain fixture | `wordpress.com` was already the fixture after the Substack provider; `thenewstack.io` registration requires no change | [006-thenewstack-provider]
|
|
239
|
+
| thenewstack.io snapshot format | Verbatim first-30-line prefix (not stripped/compacted) | Blank lines must be preserved for `snapshot in result` containment assertion to pass | [006-thenewstack-provider]
|
|
190
240
|
| 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]
|
|
191
241
|
| 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]
|
|
192
242
|
| 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]
|
|
@@ -199,9 +249,9 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
199
249
|
- [x] Provider Pattern Architecture — `BaseExtractor` ABC with concrete `fetch_html` and abstract `clean_html`, `convert_to_markdown`; `MediumExtractor` inherits
|
|
200
250
|
- [x] Technology Stack — `httpx`, `beautifulsoup4`/`lxml`, `markdownify`, `pytest`, `ruff`; `uv` for all dev workflows
|
|
201
251
|
- [x] Coding Standards — PEP 8, strict type hints, `mypy --strict` passes
|
|
202
|
-
- [x] Integration Testing — real Medium URLs, snapshot-based containment assertions
|
|
252
|
+
- [x] Integration Testing — real Medium, dev.to, and Substack URLs, snapshot-based containment assertions
|
|
203
253
|
- [x] Packaging and Distribution — `pyproject.toml` + `src/` layout + `hatchling`; all Makefile targets use `uv run`
|
|
204
254
|
|
|
205
255
|
---
|
|
206
256
|
|
|
207
|
-
*Last Updated: 2026-05-
|
|
257
|
+
*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]*
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# mdfetch — Main Specification
|
|
2
2
|
|
|
3
|
-
**Last Updated**: 2026-05-
|
|
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]
|
|
3
|
+
**Last Updated**: 2026-05-16
|
|
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], [specs/005-substack-provider/spec.md], [specs/006-thenewstack-provider/spec.md]
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
`mdfetch` is a Python library that extracts article content from web platforms and returns it as clean, well-structured Markdown. The library enforces a provider pattern — an abstract base defines the extraction contract, and each supported platform is implemented as a separate, independent provider. Supported platforms: `medium.com` (and subdomains), `dev.to`.
|
|
10
|
+
`mdfetch` is a Python library that extracts article content from web platforms and returns it as clean, well-structured Markdown. The library enforces a provider pattern — an abstract base defines the extraction contract, and each supported platform is implemented as a separate, independent provider. Supported platforms: `medium.com` (and subdomains), `dev.to`, `substack.com` (and `*.substack.com` subdomains), `thenewstack.io`.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -117,6 +117,64 @@ A developer calling `extract()` encounters a transient network error. The librar
|
|
|
117
117
|
|
|
118
118
|
---
|
|
119
119
|
|
|
120
|
+
### US-011 — Extract a Free Substack Article to Markdown (P1)
|
|
121
|
+
[Source: specs/005-substack-provider]
|
|
122
|
+
|
|
123
|
+
A developer calls `extract()` with a free public `*.substack.com/p/...` URL. The function fetches the article and returns its content as clean Markdown — with no subscription banners, navigation menus, author bios, share buttons, or page chrome.
|
|
124
|
+
|
|
125
|
+
**Acceptance Scenarios**:
|
|
126
|
+
1. Given a valid URL pointing to a free public Substack post, when `extract()` is called, then it returns a non-empty Markdown string containing the article title as a top-level heading followed by the body content.
|
|
127
|
+
2. Given a Substack post with multiple headings, paragraphs, lists, and inline links, when `extract()` is called, then the returned Markdown preserves all headings, paragraphs, lists, and hyperlinks while stripping subscription CTAs and navigation elements.
|
|
128
|
+
3. Given a Substack post containing images, when `extract()` is called, then images appear in the output as Markdown image syntax (``).
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### US-012 — Handle a Paywalled Substack Post Gracefully (P2)
|
|
133
|
+
[Source: specs/005-substack-provider]
|
|
134
|
+
|
|
135
|
+
A developer calls `extract()` with a URL for a subscriber-only Substack post. The function returns the visible free-preview content as Markdown without raising an error.
|
|
136
|
+
|
|
137
|
+
**Acceptance Scenarios**:
|
|
138
|
+
1. Given a Substack post that is subscriber-only, when `extract()` is called, then it returns the freely available preview section as Markdown without raising an exception.
|
|
139
|
+
2. Given a paywalled post whose free preview contains at least one paragraph, when `extract()` is called, then the output does not contain the paywall call-to-action text (e.g., "Subscribe to read the full post").
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### US-013 — Reject Non-Article Substack Pages (P3)
|
|
144
|
+
[Source: specs/005-substack-provider]
|
|
145
|
+
|
|
146
|
+
A developer accidentally passes a Substack URL that does not point to an article (e.g., a publication homepage). The function raises a typed exception rather than returning empty or garbage Markdown.
|
|
147
|
+
|
|
148
|
+
**Acceptance Scenarios**:
|
|
149
|
+
1. Given a Substack publication homepage URL, when `extract()` is called, then `UnsupportedContentTypeError` is raised.
|
|
150
|
+
2. Given a Substack post whose extractable text is empty after stripping all chrome, when `extract()` is called, then `EmptyContentError` is raised.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### US-014 — Extract a Public thenewstack.io Article to Markdown (P1)
|
|
155
|
+
[Source: specs/006-thenewstack-provider]
|
|
156
|
+
|
|
157
|
+
A developer calls `extract()` with a thenewstack.io article URL. The function fetches the article and returns its content as clean Markdown, with no navigation menus, subscription banners, poll widgets, author bios, social share buttons, related articles sections, or any other page chrome.
|
|
158
|
+
|
|
159
|
+
**Acceptance Scenarios**:
|
|
160
|
+
1. Given a valid URL pointing to a public thenewstack.io article, when `extract()` is called, then it returns a non-empty Markdown string containing the article title as a top-level heading followed by the body content.
|
|
161
|
+
2. Given a thenewstack.io article with multiple headings, paragraphs, lists, and inline links, when `extract()` is called, then the returned Markdown preserves all headings, paragraphs, lists, code blocks, and hyperlinks while stripping navigation, subscription CTAs, poll widgets, and social share buttons.
|
|
162
|
+
3. Given a thenewstack.io article containing images, when `extract()` is called, then images appear in the output as Markdown image syntax (``).
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### US-015 — Reject Non-Article thenewstack.io Pages (P2)
|
|
167
|
+
[Source: specs/006-thenewstack-provider]
|
|
168
|
+
|
|
169
|
+
A developer passes a thenewstack.io URL that does not point to an article (e.g., the homepage, a category listing page, or a tag archive). The function raises a typed exception rather than returning empty or garbage Markdown.
|
|
170
|
+
|
|
171
|
+
**Acceptance Scenarios**:
|
|
172
|
+
1. Given the thenewstack.io homepage URL, when `extract()` is called, then `UnsupportedContentTypeError` is raised.
|
|
173
|
+
2. Given a thenewstack.io category/tag listing page URL, when `extract()` is called, then `UnsupportedContentTypeError` is raised.
|
|
174
|
+
3. Given an article page whose extractable body text is empty after stripping all chrome, when `extract()` is called, then `EmptyContentError` is raised.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
120
178
|
### US-006 — Integration Tests Pass Against Real dev.to Article URLs (P3)
|
|
121
179
|
[Source: specs/002-devto-provider]
|
|
122
180
|
|
|
@@ -164,6 +222,31 @@ A developer runs the integration test suite and all dev.to integration tests pas
|
|
|
164
222
|
- **FR-027**: The Freedium fallback MUST be unconditionally active for all Medium URL extractions — no caller configuration, opt-in flag, or extractor parameter is required or supported. [Source: specs/003-medium-freedium-fallback]
|
|
165
223
|
- **FR-028**: The Freedium fallback MUST be fully transparent to the caller — no warning, signal, metadata, or result field shall indicate which source (medium.com or Freedium) provided the content. [Source: specs/003-medium-freedium-fallback]
|
|
166
224
|
|
|
225
|
+
### Substack Platform
|
|
226
|
+
- **FR-030**: The library MUST route all `substack.com` and `*.substack.com` URLs to the Substack provider using the existing domain-registration mechanism. [Source: specs/005-substack-provider]
|
|
227
|
+
- **FR-031**: The library MUST extract the main article body from a Substack post page (`div.body.markup`) and return it as clean Markdown. [Source: specs/005-substack-provider]
|
|
228
|
+
- **FR-032**: The library MUST strip all non-content elements from a Substack article page before conversion, including: navigation headers, subscription call-to-action blocks, paywall nag prompts, social share buttons, author bio sections, comment sections, and page footers. [Source: specs/005-substack-provider]
|
|
229
|
+
- **FR-033**: The library MUST prepend the article title as a top-level Markdown heading (`# Title`) from `h1.post-title` in `div.post-header`. Because Substack's HTML structure always places the post title outside `div.body.markup`, unconditional prepend achieves exactly-once inclusion. [Source: specs/005-substack-provider]
|
|
230
|
+
- **FR-034**: The library MUST preserve the article's structural content: headings (all levels), paragraphs, ordered and unordered lists, inline code, fenced code blocks, blockquotes, hyperlinks, images, and article subtitle (when present as `h3.subtitle` in the post header). [Source: specs/005-substack-provider]
|
|
231
|
+
- **FR-035**: The library MUST raise `UnsupportedContentTypeError` when the fetched Substack page does not contain a recognisable article body element (`div.body.markup`). [Source: specs/005-substack-provider]
|
|
232
|
+
- **FR-036**: The library MUST raise `EmptyContentError` when the Substack article body is present but yields no extractable text after stripping. [Source: specs/005-substack-provider]
|
|
233
|
+
- **FR-037**: For paywalled Substack posts, the library MUST silently extract only the publicly visible free-preview section without raising an exception and without appending any truncation marker, provided the preview contains at least some text. [Source: specs/005-substack-provider]
|
|
234
|
+
- **FR-038**: The library MUST collapse runs of three or more consecutive blank lines to a single blank line in the Substack output Markdown. [Source: specs/005-substack-provider]
|
|
235
|
+
- **FR-039**: The library MUST NOT treat HTTP 429 responses from Substack as a non-retryable condition; 429 MUST be retried up to the configured retry count with the standard fixed delay. [Source: specs/005-substack-provider]
|
|
236
|
+
- **FR-040**: The library MUST convert embedded third-party content in Substack posts (e.g., tweet embeds, YouTube video iframes, and similar rich-media widgets) to plain anchor links using the embed's source URL, matching the pattern used by the dev.to provider. [Source: specs/005-substack-provider]
|
|
237
|
+
|
|
238
|
+
### The New Stack Platform
|
|
239
|
+
- **FR-041**: The library MUST route all `thenewstack.io` URLs to the TheNewStack provider using the existing domain-registration mechanism (`@register` decorator + `DOMAINS` frozenset). [Source: specs/006-thenewstack-provider]
|
|
240
|
+
- **FR-042**: The library MUST extract the main article body from a thenewstack.io page (`div#tns-post-body-content`) and return it as clean Markdown. [Source: specs/006-thenewstack-provider]
|
|
241
|
+
- **FR-043**: The library MUST strip all non-content elements from within the article body before Markdown conversion, including: sponsored content disclosures (`div.sponsored-post-disclosure`, `div.tns-sponsored-post-disclosure`, `div.sponsor-disclosure`) and injected sponsor notes (`div.tns-sponsor-note`). Navigation, VoxPop polls, social buttons, related posts, sidebar, and footer are outside the body container and require no explicit stripping. [Source: specs/006-thenewstack-provider]
|
|
242
|
+
- **FR-044**: The library MUST prepend the article title as a top-level Markdown heading (`# Title`) from `h1.title` in `div#tns-post-headline`, followed immediately by the article deck/subtitle as a plain paragraph (from `div.post-excerpt` in `div#tns-post-headline`) when one is present. [Source: specs/006-thenewstack-provider]
|
|
243
|
+
- **FR-045**: The library MUST preserve the thenewstack.io article's structural content: headings (all levels), paragraphs, ordered and unordered lists, inline code, fenced code blocks, blockquotes, hyperlinks, and images. [Source: specs/006-thenewstack-provider]
|
|
244
|
+
- **FR-046**: The library MUST raise `UnsupportedContentTypeError` when the fetched thenewstack.io page does not contain a recognisable article body element (`div#tns-post-body-content`). [Source: specs/006-thenewstack-provider]
|
|
245
|
+
- **FR-047**: The library MUST raise `EmptyContentError` when the thenewstack.io article body is present but yields no extractable text after stripping. [Source: specs/006-thenewstack-provider]
|
|
246
|
+
- **FR-048**: The library MUST collapse runs of three or more consecutive blank lines to a single blank line in the thenewstack.io output Markdown. [Source: specs/006-thenewstack-provider]
|
|
247
|
+
- **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
|
+
- **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
|
+
|
|
167
250
|
### dev.to Platform
|
|
168
251
|
- **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]
|
|
169
252
|
- **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]
|
|
@@ -190,7 +273,36 @@ A developer runs the integration test suite and all dev.to integration tests pas
|
|
|
190
273
|
|-----------|------|-------------|
|
|
191
274
|
| `DOMAINS` | `frozenset[str]` | Domain suffixes this provider handles (e.g., `{"medium.com"}`, `{"dev.to"}`) |
|
|
192
275
|
|
|
193
|
-
**Invariants**: Each domain suffix registered to exactly one provider. Stateless — every call is independent. Registered providers: `MediumExtractor` (medium.com), `DevToExtractor` (dev.to).
|
|
276
|
+
**Invariants**: Each domain suffix registered to exactly one provider. Stateless — every call is independent. Registered providers: `MediumExtractor` (medium.com), `DevToExtractor` (dev.to), `SubstackExtractor` (substack.com and all `*.substack.com` subdomains), `TheNewStackExtractor` (thenewstack.io).
|
|
277
|
+
|
|
278
|
+
### Substack Post
|
|
279
|
+
[Source: specs/005-substack-provider]
|
|
280
|
+
| Attribute | Type | Description |
|
|
281
|
+
|-----------|------|-------------|
|
|
282
|
+
| `url` | `str` | A `*.substack.com/p/<slug>` URL (or equivalent custom-domain path) |
|
|
283
|
+
| `title` | `str` | Article title from `h1.post-title` in `div.post-header` |
|
|
284
|
+
| `subtitle` | `str \| None` | Optional subtitle/deck from `h3.subtitle` in `div.post-header` |
|
|
285
|
+
| `body` | `Tag` | Prose content inside `div.body.markup` |
|
|
286
|
+
|
|
287
|
+
**Validation**: Page must contain `div.body.markup`; absent → `UnsupportedContentTypeError`. Body must yield non-empty text after stripping → else `EmptyContentError`.
|
|
288
|
+
|
|
289
|
+
### Free Preview
|
|
290
|
+
[Source: specs/005-substack-provider]
|
|
291
|
+
| Attribute | Type | Description |
|
|
292
|
+
|-----------|------|-------------|
|
|
293
|
+
| `content` | `str` | Portion of a paywalled post publicly readable without a subscription |
|
|
294
|
+
|
|
295
|
+
**Boundary**: In the DOM, bounded by the last `div.subscription-widget-wrap` at the truncation point. Stripping that element silently achieves truncation.
|
|
296
|
+
|
|
297
|
+
### TheNewStack Article
|
|
298
|
+
[Source: specs/006-thenewstack-provider]
|
|
299
|
+
| Attribute | Type | Description |
|
|
300
|
+
|-----------|------|-------------|
|
|
301
|
+
| `title` | `str` | Article title from `h1.title` in `div#tns-post-headline`; prepended as `# Title` |
|
|
302
|
+
| `deck` | `str \| None` | Optional subtitle from `div.post-excerpt` in `div#tns-post-headline`; rendered as plain paragraph after title |
|
|
303
|
+
| `body` | `Tag` | Prose content inside `div#tns-post-body-content` |
|
|
304
|
+
|
|
305
|
+
**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.
|
|
194
306
|
|
|
195
307
|
### ExtractionResult (Output)
|
|
196
308
|
| Attribute | Type | Description |
|
|
@@ -257,6 +369,16 @@ caller provides URL string
|
|
|
257
369
|
- **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.
|
|
258
370
|
- **dev.to cover image**: The cover image lives in `<header class="crayons-article__header">`, not in `div#article-body` — the provider explicitly extracts and prepends it.
|
|
259
371
|
- **dev.to HTML structure changes**: If `div#article-body` is absent, `UnsupportedContentTypeError` is raised.
|
|
372
|
+
- **Substack paywalled posts**: `div.body.markup` already contains only the free-preview content; stripping `div.subscription-widget-wrap` achieves silent truncation (no error, no marker). [Source: specs/005-substack-provider]
|
|
373
|
+
- **Substack homepage URLs**: `div.body.markup` is absent → `UnsupportedContentTypeError` is raised immediately. [Source: specs/005-substack-provider]
|
|
374
|
+
- **Substack rich embeds**: `<iframe>` elements and `div[data-component-name]` containers (excluding `SubscribeWidget` and `Image2ToDOM`) are converted to plain anchor links using the embed's source URL. [Source: specs/005-substack-provider]
|
|
375
|
+
- **Substack HTTP 429**: Treated as a retryable transient error (no `_no_retry_status_codes` override) — contrasts with `MediumExtractor` which uses `frozenset({403, 429})` to trigger Freedium fallback. [Source: specs/005-substack-provider]
|
|
376
|
+
- **Substack HTML structure changes**: If Substack redesigns and removes `div.body.markup`, the extractor will require an update.
|
|
377
|
+
- **thenewstack.io non-article pages**: Homepage, category/tag listing, and author archive pages do not render `div#tns-post-body-content` → `UnsupportedContentTypeError` is raised immediately. [Source: specs/006-thenewstack-provider]
|
|
378
|
+
- **thenewstack.io VoxPop polls**: `div.tns-voxpop-screen` and `div.tns-voxpop-modal` are page-level overlay modals injected outside `div#tns-post-body-content` — confirmed via live DOM inspection. No explicit stripping is required; scoping extraction to the body container naturally excludes them. [Source: specs/006-thenewstack-provider]
|
|
379
|
+
- **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
|
+
- **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
|
+
- **thenewstack.io HTML structure changes**: If the site redesign moves content outside `div#tns-post-body-content`, the extractor will require an update.
|
|
260
382
|
|
|
261
383
|
---
|
|
262
384
|
|
|
@@ -275,6 +397,18 @@ caller provides URL string
|
|
|
275
397
|
- **SC-011**: 100% of integration tests pass against the three provided reference dev.to article URLs at the time of release. [Source: specs/002-devto-provider]
|
|
276
398
|
- **SC-012**: The dev.to provider is delivered as exactly one new file; no existing source files are modified (except `test_router.py` for expected domain-example maintenance when the provider registers `dev.to`). [Source: specs/002-devto-provider]
|
|
277
399
|
|
|
400
|
+
- **SC-021**: A free public Substack article returns Markdown that contains the full article title and body text with zero subscription prompt phrases (e.g., "Subscribe", "This post is for paid subscribers"). [Source: specs/005-substack-provider]
|
|
401
|
+
- **SC-022**: A paywalled Substack post returns a non-empty Markdown string (the free preview) without raising an exception, provided the free preview contains at least one paragraph. [Source: specs/005-substack-provider]
|
|
402
|
+
- **SC-023**: A Substack homepage URL raises `UnsupportedContentTypeError` within the normal fetch timeout. [Source: specs/005-substack-provider]
|
|
403
|
+
- **SC-024**: The extracted Markdown for any Substack article contains no consecutive blank-line runs of three or more lines. [Source: specs/005-substack-provider]
|
|
404
|
+
- **SC-025**: The Substack provider is exercised by at least one integration test using a real network request, matching the pattern established by existing providers. [Source: specs/005-substack-provider]
|
|
405
|
+
|
|
406
|
+
- **SC-026**: A public thenewstack.io article returns Markdown that contains the full article title and body text with zero non-content element fragments (navigation link text, subscription prompts, poll questions, author bio text). [Source: specs/006-thenewstack-provider]
|
|
407
|
+
- **SC-027**: Extraction of a thenewstack.io article completes within the base class 30-second fetch timeout on stable internet. [Source: specs/006-thenewstack-provider]
|
|
408
|
+
- **SC-028**: A thenewstack.io homepage URL raises `UnsupportedContentTypeError` within the normal fetch timeout. [Source: specs/006-thenewstack-provider]
|
|
409
|
+
- **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
|
+
- **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
|
+
|
|
278
412
|
---
|
|
279
413
|
|
|
280
414
|
## Assumptions
|
|
@@ -297,4 +431,4 @@ caller provides URL string
|
|
|
297
431
|
|
|
298
432
|
---
|
|
299
433
|
|
|
300
|
-
*Last Updated: 2026-05-
|
|
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]*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# [PROJECT_NAME] Constitution
|
|
2
|
+
<!-- Example: mdfetch Constitution -->
|
|
3
|
+
|
|
4
|
+
## Core Principles
|
|
5
|
+
|
|
6
|
+
### [PRINCIPLE_1_NAME]
|
|
7
|
+
<!-- Example: I. Provider Pattern Architecture -->
|
|
8
|
+
[PRINCIPLE_1_DESCRIPTION]
|
|
9
|
+
<!-- Example: The system MUST enforce a strict Provider Pattern. An abstract base class MUST be defined for all extractors. Adding new platforms MUST only require creating a new subclass, adhering to the Open/Closed Principle. Code duplication is PROHIBITED; shared logic MUST reside in the base class. -->
|
|
10
|
+
|
|
11
|
+
### [PRINCIPLE_2_NAME]
|
|
12
|
+
<!-- Example: II. Technology Stack -->
|
|
13
|
+
[PRINCIPLE_2_DESCRIPTION]
|
|
14
|
+
<!-- Example: The project MUST exclusively use: httpx (network), BeautifulSoup (parsing), Markdownify (conversion), pytest (testing), uv (package management). Direct use of pip, venv, or pip-tools is PROHIBITED. -->
|
|
15
|
+
|
|
16
|
+
### [PRINCIPLE_3_NAME]
|
|
17
|
+
<!-- Example: III. Coding Standards -->
|
|
18
|
+
[PRINCIPLE_3_DESCRIPTION]
|
|
19
|
+
<!-- Example: All functions MUST use strict Python type hinting. Codebase MUST adhere to PEP 8. Variable names, docstrings, and comments MUST use clear English vocabulary. -->
|
|
20
|
+
|
|
21
|
+
### [PRINCIPLE_4_NAME]
|
|
22
|
+
<!-- Example: IV. Testing Requirements -->
|
|
23
|
+
[PRINCIPLE_4_DESCRIPTION]
|
|
24
|
+
<!-- Example: The test suite MUST include integration tests. These tests MUST verify functionality by providing real links and asserting returned Markdown matches expected output. -->
|
|
25
|
+
|
|
26
|
+
### [PRINCIPLE_5_NAME]
|
|
27
|
+
<!-- Example: V. Packaging and Distribution -->
|
|
28
|
+
[PRINCIPLE_5_DESCRIPTION]
|
|
29
|
+
<!-- Example: The project MUST use pyproject.toml and src/ layout. All Makefile targets MUST invoke uv run <tool> rather than calling tools directly. -->
|
|
30
|
+
|
|
31
|
+
## [SECTION_2_NAME]
|
|
32
|
+
<!-- Example: Additional Constraints, Error Handling Policy, etc. -->
|
|
33
|
+
|
|
34
|
+
[SECTION_2_CONTENT]
|
|
35
|
+
<!-- Example: All failures communicated via typed exceptions only (no logging). Custom exception hierarchy with MdfetchError as base. -->
|
|
36
|
+
|
|
37
|
+
## Governance
|
|
38
|
+
<!-- Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
|
39
|
+
|
|
40
|
+
[GOVERNANCE_RULES]
|
|
41
|
+
<!-- Example: All PRs must verify compliance. Complexity must be justified. Amendment procedure: increment constitution version. Semantic versioning for governance changes. -->
|
|
42
|
+
|
|
43
|
+
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
|
44
|
+
<!-- Example: Version: 1.0.0 | Ratified: 2026-05-14 | Last Amended: 2026-05-14 -->
|