content-core 2.0.7__tar.gz → 2.2.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.
Files changed (147) hide show
  1. {content_core-2.0.7 → content_core-2.2.0}/.claude-plugin/plugin.json +1 -1
  2. {content_core-2.0.7 → content_core-2.2.0}/.codex-plugin/plugin.json +1 -1
  3. content_core-2.2.0/.gitattributes +2 -0
  4. {content_core-2.0.7 → content_core-2.2.0}/.github/workflows/claude-code-review.yml +5 -5
  5. {content_core-2.0.7 → content_core-2.2.0}/.github/workflows/create-tag.yml +2 -1
  6. {content_core-2.0.7 → content_core-2.2.0}/.github/workflows/package.yml +36 -4
  7. content_core-2.2.0/.github/workflows/publish.yml +52 -0
  8. {content_core-2.0.7 → content_core-2.2.0}/.gitignore +4 -0
  9. content_core-2.2.0/.maintainer/PROFILE.md +11 -0
  10. content_core-2.2.0/.maintainer/README.md +15 -0
  11. content_core-2.2.0/.maintainer/gotchas.md +31 -0
  12. content_core-2.2.0/.maintainer/profile.toml +51 -0
  13. content_core-2.2.0/.maintainer/release/runbook.md +128 -0
  14. content_core-2.2.0/.maintainer/release/test-matrix.md +23 -0
  15. content_core-2.0.7/CLAUDE.md → content_core-2.2.0/AGENTS.md +21 -11
  16. {content_core-2.0.7 → content_core-2.2.0}/CHANGELOG.md +47 -0
  17. content_core-2.2.0/CLAUDE.md +1 -0
  18. {content_core-2.0.7 → content_core-2.2.0}/CONTRIBUTING.md +20 -0
  19. {content_core-2.0.7 → content_core-2.2.0}/Makefile +8 -2
  20. {content_core-2.0.7 → content_core-2.2.0}/PKG-INFO +22 -12
  21. {content_core-2.0.7 → content_core-2.2.0}/README.md +12 -3
  22. {content_core-2.0.7 → content_core-2.2.0}/REFACTOR.md +1 -1
  23. {content_core-2.0.7 → content_core-2.2.0}/TEST_PLAN.md +0 -1
  24. {content_core-2.0.7 → content_core-2.2.0}/docs/cli.md +13 -2
  25. {content_core-2.0.7 → content_core-2.2.0}/docs/mcp.md +6 -1
  26. {content_core-2.0.7 → content_core-2.2.0}/docs/processors.md +11 -1
  27. {content_core-2.0.7 → content_core-2.2.0}/docs/usage.md +111 -2
  28. {content_core-2.0.7 → content_core-2.2.0}/pyproject.toml +12 -11
  29. content_core-2.2.0/src/content_core/__init__.py +52 -0
  30. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/cli.py +17 -7
  31. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/common/__init__.py +11 -0
  32. content_core-2.2.0/src/content_core/common/exceptions.py +65 -0
  33. content_core-2.2.0/src/content_core/common/messages.py +15 -0
  34. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/common/retry.py +33 -1
  35. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/config.py +45 -5
  36. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/content/identification/file_detector.py +17 -2
  37. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/extraction.py +85 -16
  38. content_core-2.2.0/src/content_core/logging.py +54 -0
  39. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/mcp/server.py +44 -9
  40. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/document/__init__.py +24 -1
  41. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/document/docling.py +3 -8
  42. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/document/epub.py +2 -1
  43. content_core-2.2.0/src/content_core/processors/document/odf.py +218 -0
  44. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/document/pdf.py +3 -2
  45. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/media/audio.py +75 -43
  46. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/media/video.py +44 -59
  47. content_core-2.2.0/src/content_core/processors/text.py +139 -0
  48. content_core-2.2.0/src/content_core/processors/url/__init__.py +217 -0
  49. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/url/bs4.py +10 -12
  50. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/url/crawl4ai.py +27 -8
  51. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/url/firecrawl.py +3 -3
  52. content_core-2.2.0/src/content_core/processors/url/youtube.py +371 -0
  53. content_core-2.2.0/tests/input_content/file.odp +0 -0
  54. content_core-2.2.0/tests/input_content/file.ods +0 -0
  55. content_core-2.2.0/tests/input_content/file.odt +0 -0
  56. {content_core-2.0.7 → content_core-2.2.0}/tests/integration/test_extraction.py +54 -0
  57. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_cli.py +8 -0
  58. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_config_file.py +43 -0
  59. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_config_v2.py +73 -2
  60. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_crawl4ai_docker.py +15 -0
  61. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_epub_extraction.py +8 -0
  62. content_core-2.2.0/tests/unit/test_exceptions.py +84 -0
  63. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_file_detector.py +106 -1
  64. content_core-2.2.0/tests/unit/test_html_file_extraction.py +162 -0
  65. content_core-2.2.0/tests/unit/test_logging.py +156 -0
  66. content_core-2.2.0/tests/unit/test_mcp_server_version.py +9 -0
  67. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_mcp_v2.py +22 -1
  68. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_media_pipeline.py +188 -1
  69. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_models_v2.py +1 -23
  70. content_core-2.2.0/tests/unit/test_office_extraction.py +340 -0
  71. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_pdf_extraction.py +22 -1
  72. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_retry.py +54 -1
  73. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_routing.py +292 -3
  74. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_text_processing.py +70 -1
  75. content_core-2.2.0/tests/unit/test_url_engine_select.py +519 -0
  76. content_core-2.2.0/tests/unit/test_youtube_parsing.py +517 -0
  77. content_core-2.2.0/uv.lock +7720 -0
  78. content_core-2.0.7/.github/workflows/publish.yml +0 -42
  79. content_core-2.0.7/src/content_core/__init__.py +0 -27
  80. content_core-2.0.7/src/content_core/common/exceptions.py +0 -70
  81. content_core-2.0.7/src/content_core/logging.py +0 -15
  82. content_core-2.0.7/src/content_core/processors/protocol.py +0 -18
  83. content_core-2.0.7/src/content_core/processors/text.py +0 -83
  84. content_core-2.0.7/src/content_core/processors/url/__init__.py +0 -151
  85. content_core-2.0.7/src/content_core/processors/url/youtube.py +0 -216
  86. content_core-2.0.7/tests/unit/test_office_extraction.py +0 -99
  87. content_core-2.0.7/tests/unit/test_url_engine_select.py +0 -142
  88. content_core-2.0.7/tests/unit/test_youtube_parsing.py +0 -163
  89. content_core-2.0.7/uv.lock +0 -6687
  90. {content_core-2.0.7 → content_core-2.2.0}/.agent-smith/index.json +0 -0
  91. {content_core-2.0.7 → content_core-2.2.0}/.agents/plugins/marketplace.json +0 -0
  92. {content_core-2.0.7 → content_core-2.2.0}/.claude-plugin/marketplace.json +0 -0
  93. {content_core-2.0.7 → content_core-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  94. {content_core-2.0.7 → content_core-2.2.0}/.github/workflows/claude.yml +0 -0
  95. {content_core-2.0.7 → content_core-2.2.0}/.github/workflows/test.yml +0 -0
  96. {content_core-2.0.7 → content_core-2.2.0}/.python-version +0 -0
  97. {content_core-2.0.7 → content_core-2.2.0}/ARCHITECTURE.md +0 -0
  98. {content_core-2.0.7 → content_core-2.2.0}/LICENSE +0 -0
  99. {content_core-2.0.7 → content_core-2.2.0}/examples/main.py +0 -0
  100. {content_core-2.0.7 → content_core-2.2.0}/prompts/content/cleanup.jinja +0 -0
  101. {content_core-2.0.7 → content_core-2.2.0}/prompts/content/summarize.jinja +0 -0
  102. {content_core-2.0.7 → content_core-2.2.0}/skills/content-core/SKILL.md +0 -0
  103. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/common/state.py +0 -0
  104. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/common/types.py +0 -0
  105. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/content/__init__.py +0 -0
  106. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/content/extraction/__init__.py +0 -0
  107. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/content/identification/__init__.py +0 -0
  108. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/content/summary/__init__.py +0 -0
  109. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/content/summary/core.py +0 -0
  110. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/mcp/__init__.py +0 -0
  111. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/models.py +0 -0
  112. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/document/docx.py +0 -0
  113. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/document/pptx.py +0 -0
  114. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/document/xlsx.py +0 -0
  115. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/media/__init__.py +0 -0
  116. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/url/jina.py +0 -0
  117. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/processors/url/reddit.py +0 -0
  118. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/py.typed +0 -0
  119. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/templated_message.py +0 -0
  120. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/tools/__init__.py +0 -0
  121. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/tools/extract.py +0 -0
  122. {content_core-2.0.7 → content_core-2.2.0}/src/content_core/tools/summarize.py +0 -0
  123. {content_core-2.0.7 → content_core-2.2.0}/tests/conftest.py +0 -0
  124. {content_core-2.0.7 → content_core-2.2.0}/tests/e2e/__init__.py +0 -0
  125. {content_core-2.0.7 → content_core-2.2.0}/tests/e2e/test_docling.py +0 -0
  126. {content_core-2.0.7 → content_core-2.2.0}/tests/e2e/test_media.py +0 -0
  127. {content_core-2.0.7 → content_core-2.2.0}/tests/e2e/test_remote.py +0 -0
  128. {content_core-2.0.7 → content_core-2.2.0}/tests/e2e/test_url_engines.py +0 -0
  129. {content_core-2.0.7 → content_core-2.2.0}/tests/e2e/test_youtube.py +0 -0
  130. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.docx +0 -0
  131. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.epub +0 -0
  132. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.md +0 -0
  133. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.mp3 +0 -0
  134. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.mp4 +0 -0
  135. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.pdf +0 -0
  136. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.pptx +0 -0
  137. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.txt +0 -0
  138. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/file.xlsx +0 -0
  139. {content_core-2.0.7 → content_core-2.2.0}/tests/input_content/new_pdf.pdf +0 -0
  140. {content_core-2.0.7 → content_core-2.2.0}/tests/integration/conftest.py +0 -0
  141. {content_core-2.0.7 → content_core-2.2.0}/tests/integration/test_cli_v2.py +0 -0
  142. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_audio_concurrency.py +0 -0
  143. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_docling_extraction.py +0 -0
  144. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_file_detector_critical.py +0 -0
  145. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_file_detector_performance.py +0 -0
  146. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_pdf_helpers.py +0 -0
  147. {content_core-2.0.7 → content_core-2.2.0}/tests/unit/test_reddit_extraction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-core",
3
- "version": "2.0.7",
3
+ "version": "2.2.0",
4
4
  "description": "Extract text content from URLs, documents, YouTube videos, Reddit posts, and audio/video files — via CLI (uvx) or MCP.",
5
5
  "author": {
6
6
  "name": "Luis Novo",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-core",
3
- "version": "2.0.7",
3
+ "version": "2.2.0",
4
4
  "description": "Extract text content from URLs, documents, YouTube videos, Reddit posts, and audio/video files — via CLI (uvx) or MCP.",
5
5
  "skills": "./skills/"
6
6
  }
@@ -0,0 +1,2 @@
1
+ # Keep maintainer configuration out of Git-generated source archives.
2
+ .maintainer/ export-ignore
@@ -12,11 +12,11 @@ on:
12
12
 
13
13
  jobs:
14
14
  claude-review:
15
- # Optional: Filter by PR author
16
- # if: |
17
- # github.event.pull_request.user.login == 'external-contributor' ||
18
- # github.event.pull_request.user.login == 'new-developer' ||
19
- # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
15
+ # Fork PRs get no repository secrets, so the review could only fail there.
16
+ # (pull_request_target would expose the secrets to untrusted code.)
17
+ # To also filter by PR author, extend this expression with `&&` -- a job
18
+ # takes a single `if:`.
19
+ if: github.event.pull_request.head.repo.full_name == github.repository
20
20
 
21
21
  runs-on: ubuntu-latest
22
22
  permissions:
@@ -53,4 +53,5 @@ jobs:
53
53
  if: steps.check.outputs.exists == 'false'
54
54
  run: |
55
55
  echo "✨ Tag v${{ steps.version.outputs.version }} has been created!"
56
- echo "🚀 This will trigger the publish workflow automatically."
56
+ echo "Publish a GitHub Release for this tag to start the PyPI workflow."
57
+ echo "Creating the tag alone does not publish to PyPI."
@@ -2,15 +2,25 @@ name: Package
2
2
 
3
3
  # Reusable packaging gate: builds the wheel and exercises all three consumer
4
4
  # surfaces (library, CLI, MCP server) from the built artifact, with the repo
5
- # off the path. Called by test.yml (PRs / main) and by publish.yml (tags), so
6
- # a tag can never publish an artifact that failed this gate.
5
+ # off the path. Called by test.yml (PRs / main) and by publish.yml (published
6
+ # GitHub Releases), so a release cannot upload an artifact that failed this gate.
7
+ #
8
+ # The validated dist/ is uploaded as an artifact so publish.yml can release the
9
+ # exact files this gate checked, instead of rebuilding on another runner.
7
10
  on:
8
11
  workflow_call:
12
+ outputs:
13
+ artifact-name:
14
+ description: "Name of the uploaded artifact holding the validated dist/"
15
+ value: ${{ jobs.package.outputs.artifact-name }}
9
16
 
10
17
  jobs:
11
18
  package:
12
19
  runs-on: ubuntu-latest
13
20
 
21
+ outputs:
22
+ artifact-name: ${{ steps.artifact.outputs.name }}
23
+
14
24
  steps:
15
25
  - uses: actions/checkout@v4
16
26
 
@@ -22,6 +32,12 @@ jobs:
22
32
  - name: Install uv
23
33
  uses: astral-sh/setup-uv@v4
24
34
 
35
+ # Unique per run attempt: upload-artifact@v4 rejects a duplicate name within
36
+ # a run, which would otherwise break "re-run failed jobs".
37
+ - name: Compute artifact name
38
+ id: artifact
39
+ run: echo "name=dist-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT"
40
+
25
41
  - name: Build wheel and sdist
26
42
  run: |
27
43
  rm -rf dist
@@ -90,10 +106,12 @@ jobs:
90
106
  uv run --isolated --no-project --with dist/content_core-*.whl \
91
107
  python "$RUNNER_TEMP/mcp_smoke.py"
92
108
 
93
- - name: Wheel version matches pyproject version
109
+ - name: Wheel version matches project and release tag
110
+ env:
111
+ RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }}
94
112
  run: |
95
113
  python - <<'PY'
96
- import glob, pathlib, re, sys
114
+ import glob, os, pathlib, re, sys
97
115
 
98
116
  # tomllib is 3.11+, this job runs on 3.10 - read the [project] version directly.
99
117
  text = pathlib.Path("pyproject.toml").read_text()
@@ -115,5 +133,19 @@ jobs:
115
133
  sys.exit(f"could not parse version from wheel name {name!r}")
116
134
  if match.group(1) != expected:
117
135
  sys.exit(f"wheel version {match.group(1)!r} != pyproject version {expected!r}")
136
+ release_tag = os.environ.get("RELEASE_TAG", "")
137
+ if release_tag and release_tag not in (expected, f"v{expected}"):
138
+ sys.exit(f"release tag {release_tag!r} != project version {expected!r}")
118
139
  print(f"version match OK: {expected}")
119
140
  PY
141
+
142
+ # Publish.yml downloads this instead of rebuilding, so the released files
143
+ # are byte-for-byte the ones validated above. On PR runs it is just an
144
+ # unused short-lived upload.
145
+ - name: Upload validated dist
146
+ uses: actions/upload-artifact@v4
147
+ with:
148
+ name: ${{ steps.artifact.outputs.name }}
149
+ path: dist/
150
+ if-no-files-found: error
151
+ retention-days: 1
@@ -0,0 +1,52 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ package:
9
+ uses: ./.github/workflows/package.yml
10
+
11
+ # Publishes the exact artifact the packaging gate validated - it is downloaded
12
+ # from the package job rather than rebuilt here, so a green gate always covers
13
+ # the files that reach PyPI (uploads are immutable, a rebuild could differ).
14
+ publish:
15
+ needs: package
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.10"
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v4
26
+
27
+ - name: Download validated dist
28
+ uses: actions/download-artifact@v4
29
+ with:
30
+ name: ${{ needs.package.outputs.artifact-name }}
31
+ path: dist
32
+
33
+ - name: Verify downloaded dist
34
+ run: |
35
+ set -euo pipefail
36
+ ls -la dist/
37
+ python - <<'PY'
38
+ import glob, sys
39
+
40
+ wheels = glob.glob("dist/content_core-*.whl")
41
+ sdists = glob.glob("dist/content_core-*.tar.gz")
42
+ if len(wheels) != 1:
43
+ sys.exit(f"expected exactly one wheel in dist/, got {wheels}")
44
+ if len(sdists) != 1:
45
+ sys.exit(f"expected exactly one sdist in dist/, got {sdists}")
46
+ print("dist/ OK:", wheels + sdists)
47
+ PY
48
+
49
+ - name: Publish to PyPI
50
+ env:
51
+ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
52
+ run: uv publish --token "$PYPI_TOKEN"
@@ -31,3 +31,7 @@ specs/
31
31
  .claude/
32
32
  # Intermediate audio extracted from test videos (see processors/media/video.py)
33
33
  tests/input_content/*_audio.mp3
34
+
35
+ # oss-maintainer local state
36
+ .maintainer/profile.local.toml
37
+ .maintainer/state/
@@ -0,0 +1,11 @@
1
+ # Scope and communication
2
+
3
+ Content Core owns its extraction and summarization API, format processors, engine
4
+ routing, configuration, CLI, MCP server, package metadata and plugin manifests.
5
+ Hosted providers, upstream dependencies and their availability are external.
6
+
7
+ State observed behavior and validation limits directly. Credit contributors in
8
+ release notes. Do not promise upstream availability or delivery dates.
9
+
10
+ Do not publish credentials, local configuration, private paths, user input content
11
+ or raw logs containing those values. Publish evidence summaries and public CI links.
@@ -0,0 +1,15 @@
1
+ # Maintainer workflows
2
+
3
+ This directory configures the oss-maintainer release workflow for Content Core.
4
+ It is not a list of maintainers.
5
+
6
+ - `profile.toml`: commands, required evidence, delivery and publication policy.
7
+ - `PROFILE.md`: scope and public communication conventions.
8
+ - `release/runbook.md`: release preparation, artifact checks and verification.
9
+ - `release/test-matrix.md`: risk-based checks; instantiate on each candidate.
10
+ - `gotchas.md`: verified limitations and lessons.
11
+ - `state/`: ignored run records, logs and artifact identities.
12
+ - `profile.local.toml`: optional ignored preferences; cannot weaken shared gates.
13
+
14
+ The canonical commands remain in `AGENTS.md`, `Makefile` and the existing GitHub
15
+ workflows. This profile configures releases only; other capabilities are not onboarded.
@@ -0,0 +1,31 @@
1
+ # Release gotchas
2
+
3
+ - `make release` publishes the existing draft GitHub Release and starts PyPI
4
+ distribution through `release: published`. It requires the exact candidate GO.
5
+ `make tag` only creates/pushes the version tag under the current workflow; saving
6
+ a draft release does not distribute to PyPI. Tags pointing to old workflow
7
+ revisions can still carry the former tag-push trigger; do not use them for a cut.
8
+ - The GitHub Release must reference the validated candidate. Verify the remote
9
+ tag's resolved commit before publication. The Package job rejects a release tag
10
+ that differs from the project version (with or without the `v` prefix).
11
+ - `published` covers releases published directly or from drafts, including
12
+ prereleases. Editing an already published release does not trigger a new upload.
13
+ - `Publish` builds again in its reusable Package job, then uploads those validated
14
+ bytes to PyPI without rebuilding in the publish job. Pre-GO and release-run builds
15
+ are separate artifacts; compare hashes and verify the actual registry files.
16
+ - `uv build` alone is not the package gate. Exercise the installed library, CLI
17
+ and real MCP initialization response outside the checkout.
18
+ - `uv run --isolated --no-project` does not itself isolate Python imports. Use
19
+ a temporary working directory, absolute wheel/fixture paths and `python -I`.
20
+ - `make ruff` modifies files. Lint and mypy are not release gates (`AGENTS.md`).
21
+ - E2E suites may call paid services or download large models. Select and authorize
22
+ their scope before execution; skips and missing credentials are not passes.
23
+ - The default `python3` on the current maintainer machine is older than the plugin
24
+ scripts require. Use an available Python >=3.11 for profile/run-record scripts;
25
+ this does not change the package's Python >=3.10 compatibility requirement.
26
+ - On 2026-09-05 the Dependabot API returned HTTP 403 with alerts disabled. Security
27
+ alerts were unverified, not absent. Recheck on each release.
28
+ - `released` is not currently a repository label. Creating it or applying release
29
+ labels requires the owner's agreement; do not create labels during bootstrap.
30
+ - `.gitattributes` export-ignore affects Git archives, not necessarily Hatch's
31
+ sdist selection. Inspect the built sdist for accidental local-state inclusion.
@@ -0,0 +1,51 @@
1
+ schema_version = 1
2
+
3
+ [project]
4
+ name = "Content Core"
5
+ repo = "lfnovo/content-core"
6
+ artifact = "pypi-library"
7
+ commands_doc = "AGENTS.md"
8
+
9
+ [commands.validator]
10
+ run = "make test"
11
+ cwd = "."
12
+ timeout = "15m"
13
+
14
+ [contributing]
15
+ conventions = "CONTRIBUTING.md"
16
+
17
+ [release]
18
+ process_doc = ".maintainer/release/runbook.md"
19
+ change_delivery = "pr"
20
+ versioning = "repository"
21
+ changelog = "CHANGELOG.md"
22
+ version_files = ["pyproject.toml", ".claude-plugin/plugin.json", ".codex-plugin/plugin.json", "uv.lock"]
23
+ lock_command = "uv sync"
24
+ distribution_trigger = "make release"
25
+ publish_workflow = ".github/workflows/publish.yml"
26
+ notes_source = "CHANGELOG.md"
27
+ consumer_surfaces = ["public Python API", "CLI commands and flags", "MCP tools and parameters", "configuration keys and environment variables", "supported formats and engines"]
28
+
29
+ [release.gates]
30
+ mandatory = ["validator", "package-gate", "bucket-c", "notes-approved"]
31
+ optional = ["security-alerts"]
32
+ not_gates = ["ruff", "mypy", "test-e2e", "test-e2e-heavy"]
33
+ merge_own_prs = "ask-once-per-session"
34
+ alerts_policy = "Report alerts when accessible. Disabled or inaccessible alerts remain not-run; never report zero alerts from an API error. Report known unresolved vulnerabilities to the owner before GO."
35
+
36
+ [artifacts.pypi]
37
+ package = "content-core"
38
+ # Existing build entry point. package-gate passes only after ALL the runbook's
39
+ # artifact-gate steps, including clean-room smokes and extras installation.
40
+ gate = "uv build"
41
+ surfaces = ["library", "cli", "mcp"]
42
+ extras = ["docling", "crawl4ai", "langchain"]
43
+ identity = "sha256"
44
+
45
+ [comms]
46
+ owner_language = "pt-BR"
47
+ public_language = "en"
48
+ agent_attribution = "none"
49
+
50
+ [channels]
51
+ announce = []
@@ -0,0 +1,128 @@
1
+ # Release runbook
2
+
3
+ ## Agreement and versioning
4
+
5
+ Follow `CONTRIBUTING.md` for PR delivery, `AGENTS.md` for canonical validation,
6
+ `ARCHITECTURE.md` for compatibility decisions and `CHANGELOG.md` for SemVer.
7
+ The existing architecture decision treats fixes to hidden extraction errors as
8
+ 2.x fixes. New supported formats and public API additions are minor changes;
9
+ breaking supported consumer contracts require explicit version review. The owner
10
+ decides the release number. Artifact: pypi-library; distribution trigger: `make release`,
11
+ which publishes the reviewed draft GitHub Release (`release: published`).
12
+
13
+ Prepare changes in a branch and PR. Required branch protections and review remain
14
+ binding. Ask once per session before merging qualifying own PRs. A merge to main
15
+ runs tests but does not publish. Creating/pushing a tag or saving a draft release
16
+ does not publish to PyPI when the tag contains the current workflow. Only the exact
17
+ final candidate GO permits `make release`.
18
+
19
+ ## Scope and source validation
20
+
21
+ Fetch tags after bootstrap. Compare the latest release with the candidate; audit
22
+ all behavior changes and their issue/PR references against the changelog. Record
23
+ the candidate SHA, pre-existing working-tree changes and chosen checks under
24
+ `.maintainer/state/`. Keep user files, including untracked files, intact.
25
+
26
+ Run `make test` from the candidate checkout and retain the log/exit status.
27
+ Require successful Test workflow jobs for Python 3.10, 3.11 and 3.12, plus Package,
28
+ on the final integrated candidate. `AGENTS.md` explicitly excludes ruff, mypy and
29
+ broad external-service E2E suites from validator gates. The selected plan defines
30
+ any optional external checks and authorized costs before those checks run.
31
+
32
+ ## Artifact gate
33
+
34
+ The canonical builder is `uv build`; the canonical smoke implementations are
35
+ in `.github/workflows/package.yml`. A successful build alone is insufficient.
36
+
37
+ 1. Use a fresh candidate checkout or an empty release-owned `dist/`; run `uv build`.
38
+ 2. Require exactly one wheel and one sdist. Compare their version with
39
+ `pyproject.toml`, both plugin manifests and the content-core entry in `uv.lock`.
40
+ 3. Run the workflow's runtime-assets inspection against this wheel. Inspect the
41
+ sdist too; exclude local state, credentials and accidental development artifacts.
42
+ 4. Resolve the wheel and fixture paths to absolute paths. In a fresh temporary
43
+ working directory, use the workflow's `uv run --isolated --no-project --with`
44
+ commands against that wheel; use `python -I` for Python probes. Prove the module
45
+ origin is an installed environment, not this checkout. Import `content_core`,
46
+ its public API and public exception exports. Call `extract_content` on the
47
+ existing `tests/input_content/file.txt` fixture and assert non-empty content.
48
+ 5. Exercise `content-core --help`, `content-core --version`, and the workflow's
49
+ real TXT and PDF extraction commands using absolute fixture paths. Check that
50
+ the JSON result contains expected extracted content.
51
+ 6. Run the workflow's real stdio `initialize` handshake with `content-core-mcp`
52
+ from the isolated directory. Require a JSON-RPC result without an error and
53
+ `serverInfo.version` equal to the candidate version. Retain stdout/stderr.
54
+ 7. Install each declared extra (`docling`, `crawl4ai`, `langchain`) with the exact
55
+ wheel in an isolated environment and check the dependency installation succeeds.
56
+ Installation is not evidence of successful live provider calls or model runs.
57
+ 8. Record SHA-256 for both artifacts and link every result to the tested bytes.
58
+ Retain artifacts until post-publication verification finishes.
59
+
60
+ The release agent adapts the existing workflow commands to absolute paths in the
61
+ execution plan, before running them. It must not substitute editable/source tests
62
+ for these checks. Re-run affected checks after fixes and the full final artifact
63
+ gate after the version cut is integrated.
64
+
65
+ ## Manual scope
66
+
67
+ The matrix distinguishes automated regression coverage from optional live checks.
68
+ The owner reviews results and explicit unverified paths; that acknowledgment is
69
+ `bucket-c`. If a manual check is made mandatory for the candidate, it must actually
70
+ pass. Missing credentials remain unverified and never receive an inferred pass.
71
+ Neither this profile nor its approval authorizes paid calls or large model runs.
72
+
73
+ ## Cut and notes
74
+
75
+ Bump `pyproject.toml`, `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`
76
+ together, then use the existing `uv sync` command to update `uv.lock`. Confirm only
77
+ intended lock changes and verify all package-version values agree. Leave the
78
+ marketplace's independent schema/catalog version unchanged.
79
+
80
+ Date the versioned changelog in the owner's timezone and open a fresh Unreleased
81
+ section. Commit all cut files together through the PR process. Collect contributor
82
+ credits and approve the exact notes before publication.
83
+
84
+ After the final candidate is integrated and validated, prepare its tag with
85
+ `make tag` from that exact commit. Verify the remote tag resolves to the candidate.
86
+ Create a draft with `gh release create <tag> --verify-tag --draft --title <version>
87
+ --notes-file .maintainer/state/release-notes.md`. A draft is preparation, not GO.
88
+ Do not use a tag from an older commit whose workflow still publishes on tag push.
89
+
90
+ ## Publish — only after GO
91
+
92
+ Present all mandatory results, manual acknowledgment, security visibility, exact
93
+ candidate SHA, wheel/sdist SHA-256, approved notes and `make release`. Immediately
94
+ before the trigger, recheck HEAD, version files, the remote tag's resolved commit,
95
+ and the draft release's tag, notes and draft status. Confirm the version is still
96
+ absent from PyPI.
97
+
98
+ Run `make release` once on the approved candidate. It uses
99
+ `gh release edit <tag> --draft=false --latest`; publishing the draft emits
100
+ `release: published` and starts `.github/workflows/publish.yml`. Publishing a
101
+ release directly in GitHub is the same distribution action and requires the same
102
+ GO. Draft creation, tag pushes and edits to an already published release do not
103
+ start this workflow. The published event includes prereleases; their tag must
104
+ match the project's version (optionally prefixed with `v`).
105
+
106
+ Observe Publish to completion. Its Package job checks out the release tag, builds
107
+ and validates the artifacts, and checks the tag against the package version before
108
+ upload. A failure requires an owner decision; do not delete/recreate the release,
109
+ retry through another path or overwrite a version to work around it. Verify the
110
+ published release page still contains the approved notes.
111
+
112
+ ## Post-publication verification
113
+
114
+ Download both published files from PyPI for the exact version and record their
115
+ SHA-256 separately from tested hashes. Compare them with the pre-GO artifacts and
116
+ the release-run Package artifact. The release workflow builds afresh, so report differences
117
+ and repeat the artifact smokes on the actual published wheel. Install the exact
118
+ version from PyPI in a fresh environment outside the checkout and repeat library,
119
+ CLI and MCP probes. Check the release page's tag, approved notes and latest status.
120
+
121
+ ## Cleanup and closure
122
+
123
+ Record announcement disposition (none configured; the owner posts any later
124
+ approved text). Do not create/apply release labels without agreement.
125
+ Stop only processes started by this run and remove only its temporary environments,
126
+ fixtures and artifacts after retaining required evidence. Check the working tree
127
+ against its initial state. Record publish, verify, announcement disposition and
128
+ cleanup before closing the run with GO. Retro work is independent of delivery.
@@ -0,0 +1,23 @@
1
+ # Release test matrix
2
+
3
+ Instantiate against the final candidate before execution. For 2.1.0, compare v2.0.7 with the final candidate: HTML ingestion, engine/configuration
4
+ errors, public exceptions, logging, MCP version, packaging and release publication.
5
+ Record the concrete SHA and artifact hashes in the run record.
6
+
7
+ | Bucket / risk | Command or concrete action | Expected evidence | Resources / authority | Timing / disposition |
8
+ |---|---|---|---|---|
9
+ | A: shared routing/configuration, exception, HTML, logging and MCP regressions | `make test` | No unit/integration failures; log and candidate SHA | Local fixtures, no API calls | Pre-GO, mandatory validator |
10
+ | A: Python compatibility | Existing Test workflow on final candidate, Python 3.10/3.11/3.12 | All three jobs passed with matching SHA | GitHub CI | Pre-GO, required project checks |
11
+ | A: wheel/sdist and consumer surfaces | `uv build`, then every Artifact gate step in runbook; existing Package workflow | Installed library call, CLI TXT/PDF extraction, real MCP response, assets/version agreement and both SHA-256 values | Isolated environments; package downloads; no paid API calls | Pre-GO, mandatory package-gate |
12
+ | A: optional dependency installation | Install exact wheel with each of docling/crawl4ai/langchain, as runbook specifies | All extras install; do not claim live engine coverage | Package downloads, potentially large ML dependencies; no model inference | Pre-GO, part of package-gate |
13
+ | A: release trigger and version identity | Validate workflow syntax; exercise Package version check with matching/mismatched release tags and wheel versions; dry-run Make targets | Only `release: published` triggers Publish; draft/tag preparation does not upload; mismatched versions fail; Package artifacts feed upload | Local workflow checks, no publication | Pre-GO, required for workflow changes |
14
+ | C: owner review of evidence and gaps | Review results and named unverified provider paths; approve exact notes including credits | Owner acknowledgment and exact approved text | Owner | Pre-GO, bucket-c and notes-approved |
15
+ | C: optional live URL routing | After scoped approval: `uv run pytest tests/e2e/test_url_engines.py -v -m e2e` | Per-test results; skips explicitly unverified | Network; Firecrawl credentials/usage, Jina and Crawl4AI environment; execution/count/cost requires separate agreement | Pre-GO signal; not a mandatory broad E2E gate |
16
+ | C: optional real Docling baseline | After scoped approval: `uv run pytest tests/e2e/test_docling.py::test_docling_pdf_extraction -v -m e2e_heavy` | Non-empty PDF extraction via explicit Docling | Large model downloads and local compute; separate scope approval | Pre-GO signal; unrun means unverified |
17
+ | C: unchanged media/network baselines | Select existing node IDs from tests/e2e/test_media.py, test_youtube.py and test_remote.py after credential/cost review | Per-test observed outcomes | Network; STT provider may incur cost | Optional; unrun means unverified |
18
+ | Signal: security alerts | Read repository Dependabot alerts; report unavailable API separately | Alert details or explicit not-run reason | Read-only GitHub access | Pre-GO, optional signal requiring honest disclosure |
19
+ | A: registry delivery | Download exact PyPI version, compare hashes and repeat isolated library/CLI/MCP smokes | Actual published digests, working surfaces, correct release page | Network, fresh environment | Post-publication, required delivery verification |
20
+
21
+ Bucket B: no new automation project is selected. Existing unit/integration and CI
22
+ packaging checks cover the deterministic changes. Any investment proposal is a
23
+ separate owner decision. This plan does not authorize paid/live suite execution.
@@ -37,7 +37,7 @@ uv sync --group dev
37
37
  uv run pytest tests/unit tests/integration -v
38
38
  ```
39
39
 
40
- This is the gate. It returns clean pass/fail in ~15s, requires no credentials or network, and matches what CI enforces on `main` (CI runs only `tests/unit`; integration tests are also safe locally — no network/API keys). For targeted feedback during iteration, use `uv run pytest -k "<keyword>"` per the table in the Testing section below.
40
+ This is the gate. It returns clean pass/fail in ~15s and requires no credentials or network. CI on PRs and `main` runs exactly these tests (`tests/unit tests/integration`) across Python 3.10/3.11/3.12, plus a packaging gate (`.github/workflows/package.yml`) that builds the wheel and exercises the library, the `content-core` CLI and the `content-core-mcp` server from the built artifact. The packaging gate needs no credentials either, but it is not reproducible from a plain pytest run — if you touch packaging metadata (`pyproject.toml`, entry points, package data), expect it to be the check that catches you. For targeted feedback during iteration, use `uv run pytest -k "<keyword>"` per the table in the Testing section below.
41
41
 
42
42
  ### Do NOT use as gates
43
43
 
@@ -51,7 +51,7 @@ If you believe a task genuinely requires lint or type cleanup, do it as a *separ
51
51
 
52
52
  - Single processor changes (`processors/url/*.py`, `processors/document/*.py`, `processors/media/*.py`) are the natural unit of work — keep changes confined to one file plus its matching test in `tests/unit/`.
53
53
  - Avoid touching `extraction.py` (orchestrator) or `config.py` unless the task explicitly targets routing or configuration.
54
- - New processors should follow the `Processor` protocol in `src/content_core/processors/protocol.py`.
54
+ - New processors follow the function contract documented in `ARCHITECTURE.md` ("Two-layer contract"): a module-level `async def extract_<fmt>_file(file_path, config) -> ExtractionOutput` that raises on failure, with per-unit degradation in the inner parsing layer.
55
55
 
56
56
  ## Codebase Structure
57
57
 
@@ -76,8 +76,7 @@ src/content_core/
76
76
  │ └── identification/ # File type detection (pure Python)
77
77
  │
78
78
  ├── processors/
79
- │ ├── protocol.py # Processor Protocol definition
80
- │ ├── text.py # Plain text + HTML-to-markdown
79
+ │ ├── text.py # Plain text + HTML-to-markdown (also .html/.htm files, <title> extraction)
81
80
  │ ├── url/ # URL extraction engines
82
81
  │ │ ├── __init__.py # Engine router + fallback chain
83
82
  │ │ ├── bs4.py # BeautifulSoup + readability
@@ -93,6 +92,7 @@ src/content_core/
93
92
  │ │ ├── docx.py # python-docx
94
93
  │ │ ├── pptx.py # python-pptx
95
94
  │ │ ├── xlsx.py # openpyxl
95
+ │ │ ├── odf.py # ODT/ODS/ODP via odfpy
96
96
  │ │ └── docling.py # Optional Docling integration
97
97
  │ └── media/ # Audio/video processing
98
98
  │ ├── __init__.py # Video→audio pipeline
@@ -130,7 +130,7 @@ from content_core import ContentCoreConfig
130
130
  config = ContentCoreConfig(url_engine="firecrawl", audio_concurrency=5)
131
131
  ```
132
132
 
133
- Key settings: `url_engine`, `document_engine`, `audio_provider`, `audio_model`, `firecrawl_api_url`, `youtube_languages`, `llm_provider`, `llm_model`, `docling_ocr`, `docling_formulas`, `docling_vision`
133
+ Key settings: `url_engine`, `document_engine`, `audio_provider`, `audio_model`, `firecrawl_api_url`, `youtube_languages`, `youtube_cookies_file`, `youtube_proxy`, `llm_provider`, `llm_model`, `docling_ocr`, `docling_formulas`, `docling_vision`
134
134
 
135
135
  Docling enrichment flags (`docling_ocr`, `docling_formulas`, `docling_vision`) control OCR, formula extraction, and image/chart processing when `document_engine="docling"`. These are also exposed as CLI flags (`--formulas`, `--pictures`, `--no-ocr`) and MCP parameters.
136
136
 
@@ -167,14 +167,15 @@ When you change a specific processor or module, run only the relevant tests for
167
167
  | `processors/url/youtube.py` | `uv run pytest -k "youtube"` |
168
168
  | `processors/url/reddit.py` | `uv run pytest -k "reddit"` |
169
169
  | `processors/document/pdf.py` | `uv run pytest -k "pdf"` |
170
- | `processors/document/docx.py` or `pptx.py` or `xlsx.py` | `uv run pytest -k "office"` |
170
+ | `processors/document/docx.py` or `pptx.py` or `xlsx.py` or `odf.py` | `uv run pytest -k "office"` |
171
171
  | `processors/document/docling.py` | `uv run pytest -k "docling"` |
172
- | `processors/text.py` | `uv run pytest -k "text_processing"` |
172
+ | `processors/text.py` | `uv run pytest -k "text_processing or html_file"` |
173
173
  | `processors/media/audio.py` | `uv run pytest -k "audio or media_pipeline"` |
174
174
  | `processors/media/video.py` | `uv run pytest -k "media_pipeline"` |
175
175
  | `mcp/server.py` | `uv run pytest -k "mcp"` |
176
176
  | `cli.py` | `uv run pytest -k "cli"` |
177
177
  | `common/retry.py` | `uv run pytest -k "retry"` |
178
+ | `common/exceptions.py` | `uv run pytest -k "exceptions"` |
178
179
  | `content/identification/` | `uv run pytest -k "file_detector"` |
179
180
  | `common/state.py` (models) | `uv run pytest -k "models"` |
180
181
 
@@ -191,20 +192,22 @@ tests/
191
192
  │ ├── test_pdf_extraction.py # PDF text cleaning, extraction with mocked pdfplumber
192
193
  │ ├── test_pdf_helpers.py # Formula detection, table conversion helpers
193
194
  │ ├── test_epub_extraction.py # EPUB extraction with mocked fast-ebook
194
- │ ├── test_office_extraction.py # DOCX/PPTX/XLSX routing and extraction
195
+ │ ├── test_office_extraction.py # DOCX/PPTX/XLSX/ODT/ODS/ODP routing and extraction
195
196
  │ ├── test_docling_extraction.py # Docling output formats with mocked converter
196
- │ ├── test_text_processing.py # HTML detection, markdown conversion
197
+ │ ├── test_text_processing.py # HTML detection, markdown conversion, <title>/<head> handling
198
+ │ ├── test_html_file_extraction.py # Local .html file → markdown via text processor
197
199
  │ ├── test_media_pipeline.py # Audio transcription, video pipeline, stream selection
198
200
  │ ├── test_audio_concurrency.py # Semaphore, ordering, concurrency limits
199
201
  │ ├── test_mcp_v2.py # MCP tools: extract + summarize
200
202
  │ ├── test_cli.py # CLI: _build_input, commands, config subcommands
201
203
  │ ├── test_config_file.py # TOML config file: read/write, set/delete, precedence
202
- │ ├── test_models_v2.py # ExtractionInput/Output, Processor Protocol
204
+ │ ├── test_models_v2.py # ExtractionInput/Output data models
203
205
  │ ├── test_retry.py # Retry decorators, exception classification
206
+ │ ├── test_exceptions.py # Exception taxonomy + public exports
204
207
  │ └── test_file_detector*.py # MIME detection, performance, edge cases
205
208
  │
206
209
  ├── integration/ # Local files, no network (~22 tests)
207
- │ ├── test_extraction.py # Real file extraction (PDF, DOCX, PPTX, XLSX, EPUB, etc.)
210
+ │ ├── test_extraction.py # Real file extraction (PDF, DOCX, PPTX, XLSX, ODT/ODS/ODP, EPUB, HTML, etc.)
208
211
  │ └── test_cli_v2.py # CLI subcommands via CliRunner with real extraction
209
212
  │
210
213
  └── e2e/
@@ -214,3 +217,10 @@ tests/
214
217
  ├── [e2e] test_media.py # Audio/video transcription via STT API
215
218
  └── [e2e_heavy] test_docling.py # Docling extraction with enrichment flags
216
219
  ```
220
+
221
+ ## Maintainer workflows
222
+
223
+ Maintainer profile lives in `.maintainer/`; do not run release, triage or discussions workflows without it.
224
+ PyPI publication is triggered by a published GitHub Release (`release: published`).
225
+ `make tag` prepares the tag; `make release` publishes its reviewed draft release.
226
+ Follow `.maintainer/release/runbook.md` for candidate validation and publication.
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.0] - 2026-10-03
11
+
12
+ ### Added
13
+ - OpenDocument extraction for `.odt`, `.ods` and `.odp` through the office route, using `odfpy` (new core dependency). Text documents become markdown paragraphs, headings, lists and tables; spreadsheets become one markdown table per sheet; presentations become one block per slide. Detection reads the ZIP's `mimetype` member, so a wrong or missing extension still works (#43).
14
+ - YouTube cookie and proxy support for the transcript path (#46). `youtube_cookies_file` (`CCORE_YOUTUBE_COOKIES_FILE`) loads a Netscape `cookies.txt` into the `youtube-transcript-api` session, unblocking IP-flagged networks; a missing or unreadable file raises `ConfigurationError`. `youtube_proxy` (`CCORE_YOUTUBE_PROXY`) routes both `youtube-transcript-api` and the `pytubefix` fallback through a proxy URL — use a residential proxy, as datacenter proxies are blocked. With neither set, behavior is unchanged. Cookie values are never logged.
15
+ - `audio_segment_minutes` setting (`CCORE_AUDIO_SEGMENT_MINUTES`) controls the length of the segments long audio is split into before transcription. The default stays 10 minutes; `0` sends the file whole, for self-hosted STT endpoints without an upload size limit (#94).
16
+
17
+ ### Changed
18
+ - **URL extraction failures now raise instead of returning empty output** (#60). `content=""` now only means the source was genuinely empty. A named `url_engine` that fails raises `NetworkError` (connection/timeout/DNS), `NotFoundError` (HTTP 404/410 from the page, `simple` engine), `ExternalServiceError` (engine API or HTTP errors) or `ConfigurationError` (`crawl4ai` not installed). `auto` still falls through firecrawl → jina → crawl4ai → simple and raises the last error only when every engine failed. A malformed URL raises `InvalidInputError`. The `simple` engine no longer returns `"Failed to extract content: ..."` or `"No content found"` as page content. Remote-file downloads raise the same typed errors, and a download that cannot be saved locally raises `FileOperationError`.
19
+ - File extraction failures raise typed errors (#60): corrupted PDF, EPUB or unreadable text files raise `FileOperationError` (chained to the original cause) instead of bare `Exception`/`RuntimeError`; a missing file still raises `FileNotFoundError`. ffmpeg/ffprobe failures in audio and video processing raise `FileOperationError`: video extraction no longer returns empty content when ffprobe or ffmpeg fails. Speech-to-text provider failures (after retries) raise `ExternalServiceError`, and an STT model that cannot be created raises `ConfigurationError`.
20
+ - Retry classification now treats `ConfigurationError`, `UnsupportedTypeException`, `InvalidInputError` and `FileOperationError` as permanent and `NetworkError` as transient (#60).
21
+ - YouTube extraction raises instead of returning empty content when no transcript can be fetched (#60): `NoTranscriptFound` when neither youtube-transcript-api nor pytubefix finds a transcript, `ExternalServiceError` (chained to the provider error) when YouTube blocks or fails the request on both paths, e.g. `IpBlocked`, and `InvalidInputError` for a YouTube URL without a video ID. Provider errors from `get_best_transcript` and `extract_transcript_pytubefix` now propagate after retries instead of being swallowed (`extract_transcript_pytubefix` still returns `(None, None)` for a video without captions); a failure of the first path still falls back to the second.
22
+ - An explicit `audio_provider`/`audio_model` that cannot be created now raises `ConfigurationError` instead of silently transcribing with the default STT model, per "Explicit choice is a requirement" (#99).
23
+ - youtube-transcript-api failures are retried by type: blocked requests (`RequestBlocked`/`IpBlocked`) are retried, so a rotating proxy can get a new IP; other transcript errors that name a state of the video (unavailable, transcripts disabled, age-restricted...) are not (#99).
24
+
25
+ ### Fixed
26
+ - `youtube_proxy` now also applies to the YouTube video title lookup, which previously used only env-var proxies (#99).
27
+ - `requests`, imported directly for the YouTube cookie session, is declared as a dependency instead of arriving transitively (#99).
28
+ - A failed Crawl4AI crawl (navigation timeout, blocked page) now raises `ExternalServiceError` instead of failing with a pydantic `ValidationError` on `content=None`, and `auto` falls through to the next engine. Applies to both the local browser and the Docker API mode (#101).
29
+
30
+ ### Security
31
+ - Raised minimum versions to releases without known vulnerabilities: `aiohttp>=3.14.3`, `pillow>=12.3.0`, `click>=8.3.3`, `pydantic-settings>=2.14.2`, and in the extras `docling>=2.94.0`, `crawl4ai>=0.9.0` and `langchain-core>=1.3.3`. Upgrading content-core upgrades these packages in an existing environment. The development lock was refreshed; it now also exercises `fastmcp` 4 and `mcp` 2, which fresh installs already resolve to (#102).
32
+
33
+ ## [2.1.0] - 2026-09-06
34
+
35
+ ### Added
36
+ - Local HTML file ingestion through the simple engine, including `.html`/`.htm` routing, HTML-to-Markdown conversion, document titles, minimal HTML documents, BOMs and legacy character encodings (#59, #66).
37
+ - Public exception exports from `content_core`: `ContentCoreError`, `UnsupportedTypeException`, `InvalidInputError`, `ConfigurationError`, `NotFoundError`, `NoTranscriptFound`, `NetworkError`, `ExternalServiceError` and `FileOperationError` (#50, #52). Typed failures can be caught through the public API. Some existing failures still escape as untyped exceptions until #60; exporting the taxonomy does not complete that migration.
38
+
39
+ ### Changed
40
+ - PyPI publication starts when a GitHub Release is published, including publishing a draft. Creating or pushing a tag alone no longer publishes. The packaging gate verifies that the release tag matches the package version (#93).
41
+ - Library imports no longer configure process-wide Loguru handlers. Content Core logging is disabled by default for library consumers; use `logger.enable("content_core")` with the host application's handlers, or `content_core.configure_logging()` from an application entry point. CLI and MCP entry points configure their own stderr logging and honor debug settings (#47).
42
+ - Published wheel and sdist now come directly from the artifacts validated by the publishing workflow's packaging job, rather than being rebuilt in the upload job (#57).
43
+
44
+ ### Removed
45
+ - Unused `asciidoc` dependency (#58).
46
+ - Unused internal `Processor` Protocol; processor contracts remain documented in `ARCHITECTURE.md` (#53).
47
+ - Unused internal exception classes `DatabaseOperationError`, `AuthenticationError` and `RateLimitError` (#52). None had a raise site; direct imports from `content_core.common.exceptions` must be updated. The replacement taxonomy is public, but provider error wrapping is still incomplete pending #60.
48
+
49
+ ### Fixed
50
+ - Explicit `document_engine="docling"` now raises `ConfigurationError` when the optional engine is unavailable, instead of silently using the simple engine. The error names `pip install "content-core[docling]"` and the `CCORE_DOCUMENT_ENGINE=simple` alternative. `auto` still permits fallback, and `check_file_support` returns an unsupported verdict with the reason (#50).
51
+ - Invalid engine names are rejected at configuration entry points, including constructor arguments, environment variables, TOML files and `content-core config set`. Misspelled boolean values are also rejected before writing; CLI/MCP validate engine names for the input type (#51).
52
+ - MCP `serverInfo.version` reports the installed Content Core version instead of FastMCP's version (#56).
53
+
54
+ ### Documentation
55
+ - Documented Firecrawl-compatible backends through `FIRECRAWL_API_URL` (#86).
56
+
10
57
  ## [2.0.7] - 2026-09-02
11
58
 
12
59
  ### Added
@@ -0,0 +1 @@
1
+ @AGENTS.md