any2md 1.0.1__tar.gz → 1.0.3__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 (120) hide show
  1. {any2md-1.0.1 → any2md-1.0.3}/.gitignore +1 -0
  2. {any2md-1.0.1 → any2md-1.0.3}/CHANGELOG.md +121 -0
  3. any2md-1.0.3/CODE_OF_CONDUCT.md +37 -0
  4. {any2md-1.0.1 → any2md-1.0.3}/PKG-INFO +73 -23
  5. {any2md-1.0.1 → any2md-1.0.3}/README.md +72 -22
  6. any2md-1.0.3/SECURITY.md +83 -0
  7. {any2md-1.0.1 → any2md-1.0.3}/any2md/__init__.py +1 -1
  8. {any2md-1.0.1 → any2md-1.0.3}/any2md/cli.py +12 -8
  9. {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/__init__.py +5 -3
  10. {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/docx.py +20 -7
  11. {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/html.py +6 -5
  12. {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/pdf.py +8 -7
  13. {any2md-1.0.1 → any2md-1.0.3}/any2md/converters/txt.py +17 -5
  14. {any2md-1.0.1 → any2md-1.0.3}/any2md/frontmatter.py +50 -19
  15. any2md-1.0.3/any2md/heuristics.py +604 -0
  16. {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/__init__.py +6 -0
  17. {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/cleanup.py +58 -19
  18. {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/structured.py +32 -2
  19. {any2md-1.0.1 → any2md-1.0.3}/any2md/pipeline/text.py +203 -8
  20. any2md-1.0.3/assets/logo-original.jpeg +0 -0
  21. any2md-1.0.3/assets/logo.png +0 -0
  22. any2md-1.0.3/assets/social-preview.jpg +0 -0
  23. {any2md-1.0.1 → any2md-1.0.3}/docs/architecture.md +201 -63
  24. {any2md-1.0.1 → any2md-1.0.3}/docs/cli-reference.md +40 -1
  25. {any2md-1.0.1 → any2md-1.0.3}/docs/output-format.md +45 -2
  26. any2md-1.0.3/docs/superpowers/plans/2026-04-26-any2md-v1.0.2-implementation.md +307 -0
  27. any2md-1.0.3/docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md +694 -0
  28. any2md-1.0.3/docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md +133 -0
  29. {any2md-1.0.1 → any2md-1.0.3}/docs/troubleshooting.md +15 -0
  30. {any2md-1.0.1 → any2md-1.0.3}/docs/upgrading-from-0.7.md +1 -0
  31. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_high_fidelity.py +7 -2
  32. any2md-1.0.3/tests/cli/test_cli_no_arxiv_lookup.py +41 -0
  33. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_ocr_save_images.py +6 -3
  34. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_docx_docling.py +1 -1
  35. any2md-1.0.3/tests/integration/test_docx_mammoth_fallback.py +166 -0
  36. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_html_trafilatura.py +1 -1
  37. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_pdf_docling.py +3 -0
  38. any2md-1.0.3/tests/integration/test_pdf_pymupdf_fallback.py +76 -0
  39. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_snapshots.py +1 -2
  40. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_txt.py +1 -1
  41. any2md-1.0.3/tests/unit/pipeline/test_cleanup_html_entities.py +51 -0
  42. any2md-1.0.3/tests/unit/pipeline/test_cleanup_html_entities_double_encoded.py +45 -0
  43. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_ligatures.py +3 -1
  44. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_runner.py +11 -0
  45. any2md-1.0.3/tests/unit/pipeline/test_strip_orphan_punctuation.py +40 -0
  46. any2md-1.0.3/tests/unit/pipeline/test_structured_body_cleanup_stages.py +84 -0
  47. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_compact_tables.py +2 -6
  48. any2md-1.0.3/tests/unit/pipeline/test_text_dedupe_toc_table.py +106 -0
  49. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_dehyphenate.py +1 -4
  50. any2md-1.0.3/tests/unit/pipeline/test_text_strip_cover_artifacts.py +71 -0
  51. any2md-1.0.3/tests/unit/pipeline/test_text_strip_repeated_byline.py +45 -0
  52. any2md-1.0.3/tests/unit/pipeline/test_text_strip_web_fragments.py +35 -0
  53. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_auto_id.py +1 -3
  54. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_docling_helper.py +1 -0
  55. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_frontmatter_compose.py +100 -10
  56. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_frontmatter_helpers.py +24 -11
  57. any2md-1.0.3/tests/unit/test_heuristics.py +558 -0
  58. any2md-1.0.1/tests/integration/test_docx_mammoth_fallback.py +0 -42
  59. any2md-1.0.1/tests/integration/test_pdf_pymupdf_fallback.py +0 -37
  60. {any2md-1.0.1 → any2md-1.0.3}/CONTRIBUTING.md +0 -0
  61. {any2md-1.0.1 → any2md-1.0.3}/LICENSE +0 -0
  62. {any2md-1.0.1 → any2md-1.0.3}/any2md/__main__.py +0 -0
  63. {any2md-1.0.1 → any2md-1.0.3}/any2md/_docling.py +0 -0
  64. {any2md-1.0.1 → any2md-1.0.3}/any2md/config.py +0 -0
  65. {any2md-1.0.1 → any2md-1.0.3}/any2md/utils.py +0 -0
  66. {any2md-1.0.1 → any2md-1.0.3}/any2md/validators.py +0 -0
  67. {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md +0 -0
  68. {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase2-docling-backend.md +0 -0
  69. {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase3-figures-ocr-text-lane.md +0 -0
  70. {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/plans/2026-04-26-any2md-v1-phase4-polish-config-docs.md +0 -0
  71. {any2md-1.0.1 → any2md-1.0.3}/docs/superpowers/specs/2026-04-26-any2md-v1-design.md +0 -0
  72. {any2md-1.0.1 → any2md-1.0.3}/pyproject.toml +0 -0
  73. {any2md-1.0.1 → any2md-1.0.3}/requirements.txt +0 -0
  74. {any2md-1.0.1 → any2md-1.0.3}/scripts/validate-phase1.sh +0 -0
  75. {any2md-1.0.1 → any2md-1.0.3}/tests/__init__.py +0 -0
  76. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/__init__.py +0 -0
  77. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_backend.py +0 -0
  78. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_existing_flags.py +0 -0
  79. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_exit_codes.py +0 -0
  80. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_meta.py +0 -0
  81. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_quiet_verbose.py +0 -0
  82. {any2md-1.0.1 → any2md-1.0.3}/tests/cli/test_cli_strict.py +0 -0
  83. {any2md-1.0.1 → any2md-1.0.3}/tests/conftest.py +0 -0
  84. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/__init__.py +0 -0
  85. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/ligatures_and_softhyphens.txt +0 -0
  86. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/multi_column.pdf +0 -0
  87. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/table_heavy.docx +0 -0
  88. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/docs/web_page.html +0 -0
  89. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/make_fixtures.py +0 -0
  90. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/ligatures_and_softhyphens.md +0 -0
  91. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/multi_column.docling.md +0 -0
  92. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/multi_column.fallback.md +0 -0
  93. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/table_heavy.docling.md +0 -0
  94. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/table_heavy.fallback.md +0 -0
  95. {any2md-1.0.1 → any2md-1.0.3}/tests/fixtures/snapshots/web_page.md +0 -0
  96. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/__init__.py +0 -0
  97. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_docx_backend_override.py +0 -0
  98. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_pdf_backend_override.py +0 -0
  99. {any2md-1.0.1 → any2md-1.0.3}/tests/integration/test_save_images.py +0 -0
  100. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/__init__.py +0 -0
  101. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/__init__.py +0 -0
  102. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_footnote_markers.py +0 -0
  103. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_nfc.py +0 -0
  104. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_quotes_dashes.py +0 -0
  105. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_soft_hyphens.py +0 -0
  106. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_validate.py +0 -0
  107. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_cleanup_whitespace.py +0 -0
  108. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_heading_hierarchy.py +0 -0
  109. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_lift_figures.py +0 -0
  110. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_structured_normalize_cites.py +0 -0
  111. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_dedupe_paragraphs.py +0 -0
  112. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_dedupe_toc.py +0 -0
  113. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_repair_line_wraps.py +0 -0
  114. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_restore_lists_code.py +0 -0
  115. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/pipeline/test_text_strip_headers_footers.py +0 -0
  116. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_config.py +0 -0
  117. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_content_hash.py +0 -0
  118. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_meta_overrides.py +0 -0
  119. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_pdf_complexity.py +0 -0
  120. {any2md-1.0.1 → any2md-1.0.3}/tests/unit/test_validators.py +1 -1
@@ -38,6 +38,7 @@ input/
38
38
  template/
39
39
  test_docs/
40
40
  .worktrees/
41
+ logo/
41
42
 
42
43
  # ZERG / dev tooling
43
44
  .gsd/
@@ -4,6 +4,127 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.0.3] — 2026-04-26
8
+
9
+ Patch release. Audit on the v1.0.2-regenerated corpus surfaced two
10
+ patterns the previous release did not handle: cover-page-titled
11
+ documents whose first H2 stripped to empty produced an empty
12
+ `title` field, and Docling-lane output retained malformed `|`
13
+ table-row remnants because the lone-punctuation filter was
14
+ text-lane-only. The lane-agnostic body cleanup that v1.0.2's
15
+ text lane already had also now runs on Docling output.
16
+
17
+ ### Added
18
+ - New lane-agnostic stage **`strip_orphan_punctuation`** — drops
19
+ lines containing only `|` or `>`. Extracted from T10
20
+ `strip_web_fragments` so it can run on the structured (Docling)
21
+ lane in addition to the text (trafilatura) lane.
22
+
23
+ ### Changed
24
+ - `heuristics.refine_title` now walks all H2 lines line-by-line
25
+ and skips any whose content strips to empty after removing
26
+ markdown emphasis, instead of always returning the first
27
+ regex match. Also guards the Wikipedia-prefix strip against
28
+ emitting empty when the candidate is the bare prefix.
29
+ - `pipeline/structured.py` now extends `STAGES` with
30
+ `strip_repeated_byline` (T9), `dedupe_toc_table` (T7),
31
+ `strip_cover_artifacts` (T8), and `strip_orphan_punctuation`
32
+ so Docling output gets the same body-cleanup pass as text-lane
33
+ output. T10's short-fragment heuristic stays text-lane-only —
34
+ it would over-strip Docling's deliberate short headings.
35
+ - `pipeline/cleanup.py` `decode_html_entities` (C8) now loops
36
+ `html.unescape` until output stabilizes (max 5 iterations) so
37
+ double-encoded entities like `&` → `&` → `&` are
38
+ fully decoded. Some extractors emit doubly-encoded entities
39
+ that survived a single pass.
40
+
41
+ ### Fixes
42
+ - **Empty-title regression**: cover-page H1 documents whose first
43
+ H2 contained only markdown emphasis, NBSP-equivalent unicode,
44
+ or where the H2 regex's `\s+` class spanned a newline into the
45
+ next paragraph produced `title: ""`. Walk H2 lines line-by-line
46
+ with a non-empty guard.
47
+ - **Orphan-punctuation in Docling output**: malformed table-row
48
+ remnants (lone `|`) survived in structured-lane output because
49
+ T10 was text-lane-only. New `strip_orphan_punctuation` runs on
50
+ both lanes.
51
+ - **Double-encoded HTML entities**: `&` left a residual
52
+ `&` after a single unescape pass. Loop until stable.
53
+
54
+ ### Tests
55
+ - 4 new tests in `tests/unit/test_heuristics.py::TestRefineTitle`
56
+ covering NBSP-only H2, emphasis-only H2 (skip-and-pick-next),
57
+ all-empty-H2 fallthrough, and Wikipedia-prefix-only candidate.
58
+ - New `tests/unit/pipeline/test_strip_orphan_punctuation.py`
59
+ covering aggressive removal, conservative no-op, and preservation
60
+ of lines containing other content alongside `|`/`>`.
61
+ - New `tests/unit/pipeline/test_structured_body_cleanup_stages.py`
62
+ asserting the lane-agnostic stages are registered and that the
63
+ targeted patterns are removed from structured-lane output.
64
+ - New `tests/unit/pipeline/test_cleanup_html_entities_double_encoded.py`
65
+ covering the C8 loop.
66
+
67
+ ## [1.0.2] — 2026-04-26
68
+
69
+ Patch release. Closes issue #15 plus 8 additional quality issues
70
+ discovered during deep investigation against four real-world inputs:
71
+ arxiv academic paper, ISO/IEC 27002 standard, COMP4441 academic
72
+ DOCX, and a Wikipedia article via URL.
73
+
74
+ ### Added
75
+ - New `any2md/heuristics.py` module — pure functions for frontmatter
76
+ field refinement: `refine_title`, `refine_abstract`, `extract_authors`,
77
+ `filter_organization`, `arxiv_lookup`, `is_arxiv_filename`. Called
78
+ from `frontmatter.compose()` and from converter modules.
79
+ - New `produced_by` extension field on `SourceMeta` and frontmatter
80
+ (between `extracted_via` and `pages`). Records the software that
81
+ produced the source file (PDF `Creator`, DOCX `Application`).
82
+ Distinct from `extracted_via` which records the any2md backend
83
+ that produced the markdown.
84
+ - New shared cleanup stage **C8 `decode_html_entities`** — universal
85
+ removal of `&`, `>`, `<`, numeric entities (`—`,
86
+ `—`) from body. Code-block aware (skips fenced ` ``` ` blocks).
87
+ - New text-lane stages T7-T10 (aggressive/maximum profile only):
88
+ - **T7 `dedupe_toc_table`** — strips table-formatted TOCs that T4's
89
+ text-formatted-TOC heuristic doesn't catch. Common in academic PDFs.
90
+ - **T8 `strip_cover_artifacts`** — drops cover-page noise (QR-code
91
+ blurbs, version stamps like "Third edition 2022-02") in the first
92
+ ~30 lines, before the first H2.
93
+ - **T9 `strip_repeated_byline`** — removes "Author's Contact
94
+ Information:" and similar lines that duplicate a byline.
95
+ - **T10 `strip_web_fragments`** — drops trafilatura extraction
96
+ fragments (orphan `|` / `>` lines, short incomplete sentences
97
+ surrounded by blank lines).
98
+ - New CLI flag `--no-arxiv-lookup` to disable the arxiv API metadata
99
+ enrichment. Arxiv enrichment is on by default for filenames matching
100
+ `\d{4}\.\d{4,5}` (e.g., `2501.17755v1.pdf`); SSRF-guarded with 5s
101
+ timeout; failures emit non-blocking warnings.
102
+ - Project logo and brand assets under `assets/`.
103
+
104
+ ### Fixes
105
+ The eleven quality issues — see issue #15 and
106
+ `docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md`:
107
+ - A1: authors not extracted from PDF body byline (academic PDFs).
108
+ - A2: abstract picked the byline / cover blurb / TOC line.
109
+ - A3: organization populated with PDF Creator software junk.
110
+ - B1: HTML entities leaked to body universally.
111
+ - C1: ISO/TR titles detected as "INTERNATIONAL STANDARD" cover header.
112
+ - C3: DOCX titles concatenated course code + project title.
113
+ - C4: Wikipedia titles kept "Wikipedia:" namespace prefix.
114
+ - D1: TOCs dumped as markdown tables in academic PDFs.
115
+ - D2: trafilatura fragments leaked into web outputs.
116
+ - D3: ISO cover-page QR-code blurb leaked into body.
117
+ - E1: "Author's Contact Information:" line duplicated byline.
118
+
119
+ ### Changed
120
+ - `frontmatter.compose()` now consults `heuristics.py` to refine
121
+ title / abstract / authors before YAML emission.
122
+ - `--profile conservative` now also gates the new heuristic
123
+ aggressiveness (skip-lists active; speculative inferences off).
124
+ - PDF `Creator` software values (LaTeX, acmart, Adobe InDesign,
125
+ Microsoft Word, etc.) no longer populate `organization`. They
126
+ go to the new `produced_by` field instead.
127
+
7
128
  ## [1.0.1] — 2026-04-26
8
129
 
9
130
  Patch release. Adds an explicit backend-selection CLI flag.
@@ -0,0 +1,37 @@
1
+ # Code of Conduct
2
+
3
+ This project adopts the **[Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)**
4
+ as its code of conduct. The full text is available at the link above and
5
+ the canonical source is at https://www.contributor-covenant.org/.
6
+
7
+ ## Scope
8
+
9
+ The covenant applies to all project spaces, including but not limited to:
10
+
11
+ - The GitHub repository (issues, pull requests, discussions, wiki, code
12
+ review comments, commit messages).
13
+ - Any community channel that explicitly identifies itself as a project
14
+ space (mailing list, chat room, project event).
15
+ - Public spaces when an individual is officially representing the project.
16
+
17
+ ## Reporting
18
+
19
+ Reports of abuse, harassment, or otherwise unacceptable behavior may be
20
+ sent to **rock@rockcyber.com**. Reports are confidential and will be
21
+ reviewed within 5 business days.
22
+
23
+ If the report concerns the maintainer, please contact a trusted
24
+ intermediary or use a public reporting channel of your choice.
25
+
26
+ ## Enforcement
27
+
28
+ The maintainer is responsible for enforcement and may take any action
29
+ deemed appropriate, ranging from a private warning to a permanent ban
30
+ from project spaces. Enforcement actions follow the
31
+ [Contributor Covenant Enforcement Guidelines](https://www.contributor-covenant.org/version/2/1/code_of_conduct/#enforcement-guidelines).
32
+
33
+ ## Attribution
34
+
35
+ Code of conduct adapted from the
36
+ [Contributor Covenant](https://www.contributor-covenant.org), version 2.1,
37
+ available at https://www.contributor-covenant.org/version/2/1/code_of_conduct/.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: any2md
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Convert PDF, DOCX, HTML, and TXT files — or web pages by URL — to clean, LLM-optimized Markdown with YAML frontmatter.
5
5
  Project-URL: Homepage, https://github.com/rocklambros/any2md
6
6
  Project-URL: Issues, https://github.com/rocklambros/any2md/issues
@@ -28,10 +28,25 @@ Provides-Extra: high-fidelity
28
28
  Requires-Dist: docling>=2.0.0; extra == 'high-fidelity'
29
29
  Description-Content-Type: text/markdown
30
30
 
31
+ <p align="center">
32
+ <img src="assets/logo.png" alt="any2md" width="500"/>
33
+ </p>
34
+
31
35
  # any2md
32
36
 
33
37
  Convert PDFs, DOCX, HTML, URLs, and plain text into structured, machine-consumable Markdown for downstream RAG pipelines.
34
38
 
39
+ ## What's new in 1.0.3
40
+
41
+ A patch release closing two regressions surfaced by audit on the v1.0.2-regenerated corpus, plus a Docling-lane reach for the v1.0.2 body-cleanup stages:
42
+
43
+ - **Empty-title fix**: cover-page-titled documents (`# INTERNATIONAL STANDARD` etc.) whose first H2 stripped to empty (markdown emphasis only, NBSP-equivalent unicode, regex span crossing into the next paragraph) emitted `title: ""`. `heuristics.refine_title` now walks H2 lines line-by-line and skips any that strip to empty after dropping markdown emphasis. The Wikipedia-prefix strip got the same non-empty guard.
44
+ - **Docling-lane orphan-punctuation**: lone `|` or `>` lines from Docling's malformed table parsing leaked through because T10 `strip_web_fragments` was text-lane-only by design. The orphan-punctuation portion is extracted into a new lane-agnostic stage `strip_orphan_punctuation` that runs on Docling output too. T10's trafilatura-specific short-fragment heuristic stays text-lane-only.
45
+ - **Lane-agnostic body cleanup on Docling lane**: T7 `dedupe_toc_table`, T8 `strip_cover_artifacts`, and T9 `strip_repeated_byline` are now appended to the structured-lane `STAGES` list, so Docling output gets the same body-cleanup pass that text-lane output already had.
46
+ - **Double-encoded HTML entities**: C8 `decode_html_entities` now loops `html.unescape` until output stabilizes (max 5 iterations) so `&amp;amp;` → `&amp;` → `&` is fully decoded.
47
+
48
+ The full design rationale lives in [docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md](docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md). Per-fix detail is in the [CHANGELOG](CHANGELOG.md). v1.0.2 highlights — `any2md/heuristics.py`, `produced_by`, C8, T7–T10, `--no-arxiv-lookup` — remain available; see [docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md](docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md).
49
+
35
50
  ## What this is
36
51
 
37
52
  any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SSRM-compatible (Structured Security Reasoning Markdown — a documented schema for LLM-consumable documents), so retrieval pipelines, embeddings jobs, and chunking utilities downstream can rely on field names, types, and a deterministic `content_hash` for cache invalidation. The body is NFC-normalized with LF line endings, and the heading hierarchy is guaranteed to start at H1 and not skip levels. The goal is one stable shape, regardless of whether a document started life as a scanned PDF, a Word doc, a Wikipedia page, or a plain text dump.
@@ -144,6 +159,7 @@ source_file: "safety-alignment.docx" # original filename (any2md e
144
159
  type: "docx" # source format (any2md extension field)
145
160
  word_count: 14289 # any2md extension field
146
161
  extracted_via: "docling" # which backend produced the markdown
162
+ produced_by: "Microsoft® Word for Microsoft 365" # software that produced the source (new in v1.0.2)
147
163
  ---
148
164
  ```
149
165
 
@@ -408,6 +424,10 @@ type_code = "GD"
408
424
 
409
425
  With this file in place and `--auto-id` passed, generated `document_id` values look like `CSA-2026-GD-a3f1c91d`. The resolution order, highest priority first: explicit `--meta KEY=VAL` arguments, then `--meta-file PATH` (when given), then auto-discovered `.any2md.toml`, then the tool's defaults. Full flag and config reference in [docs/cli-reference.md](docs/cli-reference.md).
410
426
 
427
+ ### Network behavior
428
+
429
+ For PDFs whose filename matches the arxiv ID pattern (`\d{4}\.\d{4,5}`, e.g. `2501.17755v1.pdf`), any2md by default queries `https://export.arxiv.org/api/query` to enrich `authors`, `abstract_for_rag`, and `date` from the official arxiv metadata. The lookup is SSRF-guarded, has a 5-second timeout, and emits a non-blocking warning on any failure — conversion proceeds either way. Pass `--no-arxiv-lookup` to disable this for airgapped environments or when you want any2md to make no outbound calls. See [docs/cli-reference.md](docs/cli-reference.md#--no-arxiv-lookup) for details.
430
+
411
431
  ### Output verbosity and exit codes
412
432
 
413
433
  The CLI has three verbosity flags that pair with a four-value exit-code contract:
@@ -442,7 +462,7 @@ The full symptom-cause-fix triage guide, plus deeper diagnosis steps and follow-
442
462
  any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy backends (Docling), one for backends that need heavier text repair (pymupdf4llm, trafilatura, mammoth, the TXT heuristic). Both lanes converge on a shared cleanup pass, and the same `frontmatter.py` module emits YAML for every input format.
443
463
 
444
464
  ```
445
- any2md v1.0
465
+ any2md v1.0.3
446
466
  │
447
467
  ┌────┴────┐
448
468
  CLI │ cli.py │ parse args, classify URL/file/dir,
@@ -459,43 +479,73 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
459
479
  │ pymupdf4llm│ mammoth+md │ │
460
480
  └────┬───────┴────┬───────┴───┬───┘
461
481
  │ │ │
482
+ │ SourceMeta carries:
483
+ │ organization, produced_by ◀ NEW in v1.0.2
484
+ │ (split via heuristics.filter_organization)
485
+ │
462
486
  structured-md structured-md text-md
463
487
  │ │ │
464
488
  └─────┬──────┴───────────┘
465
489
  ▼
466
490
  ┌─────────────────────────┐
467
491
  │ pipeline/structured.py │ structured lane
468
- │ - figure caption pass │ (trusts layout)
469
- │ - table compactor │
470
- │ - cite normalizer │
471
- │ - heading hierarchy │
492
+ │ S1 figure caption pass │ (trusts layout)
493
+ │ S2 table compactor │
494
+ │ S3 cite normalizer │
495
+ │ S4 heading hierarchy │
496
+ │ T9 strip repeated │ ◀ NEW v1.0.3
497
+ │ byline │
498
+ │ T7 dedupe TOC table │ ◀ NEW v1.0.3
499
+ │ T8 strip cover │ ◀ NEW v1.0.3
500
+ │ artifacts │
501
+ │ strip_orphan_punct │ ◀ NEW v1.0.3
472
502
  └────────────┬────────────┘
473
503
  │
474
504
  ┌────────────┴────────────┐
475
505
  │ pipeline/text.py │ text lane
476
- │ - line-wrap repair │
477
- │ - dehyphenate │
478
- │ - duplicate paragraph │
479
- │ - TOC dedupe │
480
- │ - hdr/ftr strip │
481
- │ - list/code restore │
506
+ │ T1 line-wrap repair │
507
+ │ T2 dehyphenate │
508
+ │ T9 strip repeated │ ◀ NEW (E1)
509
+ │ byline │
510
+ │ T3 dedupe paragraphs │
511
+ │ T4 dedupe TOC block │
512
+ │ T7 dedupe TOC table │ ◀ NEW (D1)
513
+ │ T5 hdr/ftr strip │
514
+ │ T10 strip web │ ◀ NEW (D2)
515
+ │ fragments │
516
+ │ ↳ strip_orphan_punct ◀ split out v1.0.3
517
+ │ T6 list/code restore │
518
+ │ T8 strip cover │ ◀ NEW (D3)
519
+ │ artifacts │
482
520
  └────────────┬────────────┘
483
521
  │
484
522
  ┌────────────┴────────────┐
485
523
  │ pipeline/cleanup.py │ shared lane
486
- │ - NFC normalize │ (always last)
487
- │ - soft hyphen strip │
488
- │ - ligature normalize │
489
- │ - quote/dash normalize │
490
- │ - whitespace collapse │
491
- │ - footnote marker strip│
492
- │ - validate │
524
+ │ C1 NFC normalize │ (always last)
525
+ │ C2 soft hyphen strip │
526
+ │ C3 ligature normalize │
527
+ │ C4 quote/dash normalize│
528
+ │ C5 whitespace collapse │
529
+ │ C8 decode HTML entities│ ◀ NEW (B1, all profiles)
530
+ │ C6 footnote marker strip│
531
+ │ C7 validate │
493
532
  └────────────┬────────────┘
494
533
  │
495
- ┌────────────┴────────────┐
496
- │ frontmatter.py │ SSRM-compatible
497
- │ - merge user overrides │ YAML emitter
534
+ │ ┌──────────────────────────┐
535
+ │ │ any2md/heuristics.py │ ◀ NEW v1.0.2
536
+ │ │ - refine_title │
537
+ │ │ - refine_abstract │
538
+ │ │ - extract_authors │
539
+ │ │ (+ optional arxiv API)│
540
+ │ │ - filter_organization │
541
+ │ └────────────┬─────────────┘
542
+ │ │
543
+ ▼ │
544
+ ┌─────────────────────────┐ │
545
+ │ frontmatter.py │ ◀─────────┘ (consults heuristics
546
+ │ - merge user overrides │ before YAML emission)
498
547
  │ - derive title/date/… │
548
+ │ - emit produced_by │ ◀ NEW v1.0.2
499
549
  │ - compute content_hash │
500
550
  │ - emit YAML │
501
551
  └────────────┬────────────┘
@@ -504,7 +554,7 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
504
554
  <output>.md ← frontmatter + cleaned body
505
555
  ```
506
556
 
507
- The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
557
+ The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. v1.0.2 added a leaf `heuristics.py` module that `frontmatter.compose()` consults to refine the title, abstract, authors, and organization fields before YAML emission; new pipeline stages C8 and T7–T10 address concrete artifacts found in academic and standards-document PDFs. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
508
558
 
509
559
  ## Migrating from v0.7
510
560
 
@@ -1,7 +1,22 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" alt="any2md" width="500"/>
3
+ </p>
4
+
1
5
  # any2md
2
6
 
3
7
  Convert PDFs, DOCX, HTML, URLs, and plain text into structured, machine-consumable Markdown for downstream RAG pipelines.
4
8
 
9
+ ## What's new in 1.0.3
10
+
11
+ A patch release closing two regressions surfaced by audit on the v1.0.2-regenerated corpus, plus a Docling-lane reach for the v1.0.2 body-cleanup stages:
12
+
13
+ - **Empty-title fix**: cover-page-titled documents (`# INTERNATIONAL STANDARD` etc.) whose first H2 stripped to empty (markdown emphasis only, NBSP-equivalent unicode, regex span crossing into the next paragraph) emitted `title: ""`. `heuristics.refine_title` now walks H2 lines line-by-line and skips any that strip to empty after dropping markdown emphasis. The Wikipedia-prefix strip got the same non-empty guard.
14
+ - **Docling-lane orphan-punctuation**: lone `|` or `>` lines from Docling's malformed table parsing leaked through because T10 `strip_web_fragments` was text-lane-only by design. The orphan-punctuation portion is extracted into a new lane-agnostic stage `strip_orphan_punctuation` that runs on Docling output too. T10's trafilatura-specific short-fragment heuristic stays text-lane-only.
15
+ - **Lane-agnostic body cleanup on Docling lane**: T7 `dedupe_toc_table`, T8 `strip_cover_artifacts`, and T9 `strip_repeated_byline` are now appended to the structured-lane `STAGES` list, so Docling output gets the same body-cleanup pass that text-lane output already had.
16
+ - **Double-encoded HTML entities**: C8 `decode_html_entities` now loops `html.unescape` until output stabilizes (max 5 iterations) so `&amp;amp;` → `&amp;` → `&` is fully decoded.
17
+
18
+ The full design rationale lives in [docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md](docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md). Per-fix detail is in the [CHANGELOG](CHANGELOG.md). v1.0.2 highlights — `any2md/heuristics.py`, `produced_by`, C8, T7–T10, `--no-arxiv-lookup` — remain available; see [docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md](docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md).
19
+
5
20
  ## What this is
6
21
 
7
22
  any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SSRM-compatible (Structured Security Reasoning Markdown — a documented schema for LLM-consumable documents), so retrieval pipelines, embeddings jobs, and chunking utilities downstream can rely on field names, types, and a deterministic `content_hash` for cache invalidation. The body is NFC-normalized with LF line endings, and the heading hierarchy is guaranteed to start at H1 and not skip levels. The goal is one stable shape, regardless of whether a document started life as a scanned PDF, a Word doc, a Wikipedia page, or a plain text dump.
@@ -114,6 +129,7 @@ source_file: "safety-alignment.docx" # original filename (any2md e
114
129
  type: "docx" # source format (any2md extension field)
115
130
  word_count: 14289 # any2md extension field
116
131
  extracted_via: "docling" # which backend produced the markdown
132
+ produced_by: "Microsoft® Word for Microsoft 365" # software that produced the source (new in v1.0.2)
117
133
  ---
118
134
  ```
119
135
 
@@ -378,6 +394,10 @@ type_code = "GD"
378
394
 
379
395
  With this file in place and `--auto-id` passed, generated `document_id` values look like `CSA-2026-GD-a3f1c91d`. The resolution order, highest priority first: explicit `--meta KEY=VAL` arguments, then `--meta-file PATH` (when given), then auto-discovered `.any2md.toml`, then the tool's defaults. Full flag and config reference in [docs/cli-reference.md](docs/cli-reference.md).
380
396
 
397
+ ### Network behavior
398
+
399
+ For PDFs whose filename matches the arxiv ID pattern (`\d{4}\.\d{4,5}`, e.g. `2501.17755v1.pdf`), any2md by default queries `https://export.arxiv.org/api/query` to enrich `authors`, `abstract_for_rag`, and `date` from the official arxiv metadata. The lookup is SSRF-guarded, has a 5-second timeout, and emits a non-blocking warning on any failure — conversion proceeds either way. Pass `--no-arxiv-lookup` to disable this for airgapped environments or when you want any2md to make no outbound calls. See [docs/cli-reference.md](docs/cli-reference.md#--no-arxiv-lookup) for details.
400
+
381
401
  ### Output verbosity and exit codes
382
402
 
383
403
  The CLI has three verbosity flags that pair with a four-value exit-code contract:
@@ -412,7 +432,7 @@ The full symptom-cause-fix triage guide, plus deeper diagnosis steps and follow-
412
432
  any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy backends (Docling), one for backends that need heavier text repair (pymupdf4llm, trafilatura, mammoth, the TXT heuristic). Both lanes converge on a shared cleanup pass, and the same `frontmatter.py` module emits YAML for every input format.
413
433
 
414
434
  ```
415
- any2md v1.0
435
+ any2md v1.0.3
416
436
  │
417
437
  ┌────┴────┐
418
438
  CLI │ cli.py │ parse args, classify URL/file/dir,
@@ -429,43 +449,73 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
429
449
  │ pymupdf4llm│ mammoth+md │ │
430
450
  └────┬───────┴────┬───────┴───┬───┘
431
451
  │ │ │
452
+ │ SourceMeta carries:
453
+ │ organization, produced_by ◀ NEW in v1.0.2
454
+ │ (split via heuristics.filter_organization)
455
+ │
432
456
  structured-md structured-md text-md
433
457
  │ │ │
434
458
  └─────┬──────┴───────────┘
435
459
  ▼
436
460
  ┌─────────────────────────┐
437
461
  │ pipeline/structured.py │ structured lane
438
- │ - figure caption pass │ (trusts layout)
439
- │ - table compactor │
440
- │ - cite normalizer │
441
- │ - heading hierarchy │
462
+ │ S1 figure caption pass │ (trusts layout)
463
+ │ S2 table compactor │
464
+ │ S3 cite normalizer │
465
+ │ S4 heading hierarchy │
466
+ │ T9 strip repeated │ ◀ NEW v1.0.3
467
+ │ byline │
468
+ │ T7 dedupe TOC table │ ◀ NEW v1.0.3
469
+ │ T8 strip cover │ ◀ NEW v1.0.3
470
+ │ artifacts │
471
+ │ strip_orphan_punct │ ◀ NEW v1.0.3
442
472
  └────────────┬────────────┘
443
473
  │
444
474
  ┌────────────┴────────────┐
445
475
  │ pipeline/text.py │ text lane
446
- │ - line-wrap repair │
447
- │ - dehyphenate │
448
- │ - duplicate paragraph │
449
- │ - TOC dedupe │
450
- │ - hdr/ftr strip │
451
- │ - list/code restore │
476
+ │ T1 line-wrap repair │
477
+ │ T2 dehyphenate │
478
+ │ T9 strip repeated │ ◀ NEW (E1)
479
+ │ byline │
480
+ │ T3 dedupe paragraphs │
481
+ │ T4 dedupe TOC block │
482
+ │ T7 dedupe TOC table │ ◀ NEW (D1)
483
+ │ T5 hdr/ftr strip │
484
+ │ T10 strip web │ ◀ NEW (D2)
485
+ │ fragments │
486
+ │ ↳ strip_orphan_punct ◀ split out v1.0.3
487
+ │ T6 list/code restore │
488
+ │ T8 strip cover │ ◀ NEW (D3)
489
+ │ artifacts │
452
490
  └────────────┬────────────┘
453
491
  │
454
492
  ┌────────────┴────────────┐
455
493
  │ pipeline/cleanup.py │ shared lane
456
- │ - NFC normalize │ (always last)
457
- │ - soft hyphen strip │
458
- │ - ligature normalize │
459
- │ - quote/dash normalize │
460
- │ - whitespace collapse │
461
- │ - footnote marker strip│
462
- │ - validate │
494
+ │ C1 NFC normalize │ (always last)
495
+ │ C2 soft hyphen strip │
496
+ │ C3 ligature normalize │
497
+ │ C4 quote/dash normalize│
498
+ │ C5 whitespace collapse │
499
+ │ C8 decode HTML entities│ ◀ NEW (B1, all profiles)
500
+ │ C6 footnote marker strip│
501
+ │ C7 validate │
463
502
  └────────────┬────────────┘
464
503
  │
465
- ┌────────────┴────────────┐
466
- │ frontmatter.py │ SSRM-compatible
467
- │ - merge user overrides │ YAML emitter
504
+ │ ┌──────────────────────────┐
505
+ │ │ any2md/heuristics.py │ ◀ NEW v1.0.2
506
+ │ │ - refine_title │
507
+ │ │ - refine_abstract │
508
+ │ │ - extract_authors │
509
+ │ │ (+ optional arxiv API)│
510
+ │ │ - filter_organization │
511
+ │ └────────────┬─────────────┘
512
+ │ │
513
+ ▼ │
514
+ ┌─────────────────────────┐ │
515
+ │ frontmatter.py │ ◀─────────┘ (consults heuristics
516
+ │ - merge user overrides │ before YAML emission)
468
517
  │ - derive title/date/… │
518
+ │ - emit produced_by │ ◀ NEW v1.0.2
469
519
  │ - compute content_hash │
470
520
  │ - emit YAML │
471
521
  └────────────┬────────────┘
@@ -474,7 +524,7 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
474
524
  <output>.md ← frontmatter + cleaned body
475
525
  ```
476
526
 
477
- The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
527
+ The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. v1.0.2 added a leaf `heuristics.py` module that `frontmatter.compose()` consults to refine the title, abstract, authors, and organization fields before YAML emission; new pipeline stages C8 and T7–T10 address concrete artifacts found in academic and standards-document PDFs. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
478
528
 
479
529
  ## Migrating from v0.7
480
530
 
@@ -0,0 +1,83 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ | Version | Status |
6
+ |---|---|
7
+ | 1.0.x | ✅ Supported (current stable). Patch releases for security and quality fixes are published as needed. |
8
+ | 0.7.x | ⚠️ Receives critical security fixes only until 2026-10-26 (six months after the v1.0.0 GA on 2026-04-26). |
9
+ | < 0.7 | ❌ Not supported. Please upgrade. |
10
+
11
+ ## Reporting a vulnerability
12
+
13
+ **Please do not file public issues for security vulnerabilities.** Public
14
+ disclosure before a fix is published puts users at risk.
15
+
16
+ Email security disclosures to **rock@rockcyber.com** with the subject line
17
+ `any2md security disclosure`. Encrypted email is welcome — request a
18
+ public key in your initial message and one will be provided.
19
+
20
+ Include in your report:
21
+
22
+ - A description of the issue and the affected component.
23
+ - Affected version(s) — e.g., `pip show any2md` output, or commit SHA.
24
+ - Reproduction steps (smallest possible repro is ideal).
25
+ - Any proof-of-concept (please redact sensitive data).
26
+ - Whether you intend to publish your own write-up; if so, a proposed
27
+ embargo date so we can coordinate.
28
+
29
+ ### Response timeline
30
+
31
+ - **Acknowledgement** within 5 business days.
32
+ - **Triage and severity assessment** within 10 business days.
33
+ - **Critical issues** (remote code execution, data exfiltration via
34
+ crafted input, supply-chain compromise of a published wheel): patched
35
+ and released within 30 days of confirmation, with a coordinated public
36
+ advisory.
37
+ - **High** (denial-of-service, information disclosure with limited blast
38
+ radius): patched within 60 days.
39
+ - **Medium / low** issues are tracked in private until a fix lands in a
40
+ scheduled release, then disclosed publicly via the GitHub Security
41
+ Advisories page.
42
+
43
+ ## Scope
44
+
45
+ In scope:
46
+
47
+ - The `any2md` library and CLI.
48
+ - The published wheels and sdists on PyPI.
49
+ - The release pipeline (`.github/workflows/publish.yml`) and any
50
+ side-effects of running the workflow.
51
+ - The default conversion pipeline (PDF / DOCX / HTML / URL / TXT) and
52
+ the optional Docling backend integration.
53
+ - The arxiv API enrichment path (network call + XML parsing).
54
+ - The SSRF guards in the URL fetcher.
55
+
56
+ Out of scope:
57
+
58
+ - Vulnerabilities in third-party dependencies (please file directly with
59
+ the upstream — Docling, PyMuPDF, mammoth, trafilatura, BeautifulSoup,
60
+ lxml, etc.). We monitor upstream advisories via Dependabot.
61
+ - Issues that require local filesystem access already granted to the
62
+ running Python process.
63
+ - Local timing attacks against your own machine.
64
+ - Content correctness issues in converted Markdown (those are
65
+ conversion-quality reports — please use the
66
+ [Conversion quality issue template](.github/ISSUE_TEMPLATE/conversion_quality.md)
67
+ instead).
68
+
69
+ ## Hardening reference
70
+
71
+ If you operate any2md in a hardened environment:
72
+
73
+ - Pass `--no-arxiv-lookup` to disable the arxiv API call.
74
+ - Pass `--max-file-size` with a budget appropriate for your workload.
75
+ - Run conversions in a sandbox that cannot reach private networks
76
+ (the SSRF guard validates resolved IPs, but defense in depth is best).
77
+ - Pin `any2md` and its extras (`any2md[high-fidelity]==<exact-version>`)
78
+ to a verified version and audit the lockfile.
79
+
80
+ ## Disclosure history
81
+
82
+ No publicly disclosed vulnerabilities to date. Future advisories will
83
+ appear at https://github.com/rocklambros/any2md/security/advisories.
@@ -1,3 +1,3 @@
1
1
  """Convert PDF, DOCX, HTML, and TXT files to LLM-optimized Markdown."""
2
2
 
3
- __version__ = "1.0.1"
3
+ __version__ = "1.0.3"
@@ -65,11 +65,7 @@ def _deep_merge_overrides(
65
65
  """
66
66
  out = dict(base)
67
67
  for key, val in overrides.items():
68
- if (
69
- key in out
70
- and isinstance(out[key], dict)
71
- and isinstance(val, dict)
72
- ):
68
+ if key in out and isinstance(out[key], dict) and isinstance(val, dict):
73
69
  out[key] = _deep_merge_overrides(out[key], val)
74
70
  else:
75
71
  out[key] = val
@@ -132,6 +128,14 @@ def main():
132
128
  action="store_true",
133
129
  help="Force the Docling backend (PDF/DOCX). Exit 1 if not installed.",
134
130
  )
131
+ parser.add_argument(
132
+ "--no-arxiv-lookup",
133
+ action="store_true",
134
+ help="Disable the arxiv API lookup that enriches frontmatter for "
135
+ "PDFs whose filename matches the arxiv ID pattern (\\d{4}.\\d{4,5}). "
136
+ "Useful in airgapped envs or when you don't want any2md to make "
137
+ "network calls. Default: arxiv lookup is enabled.",
138
+ )
135
139
  parser.add_argument(
136
140
  "--backend",
137
141
  choices=("docling", "pymupdf4llm", "mammoth"),
@@ -216,9 +220,7 @@ def main():
216
220
 
217
221
  if args.meta_file is not None:
218
222
  if not args.meta_file.is_file():
219
- print(
220
- f"Error: --meta-file not found: {args.meta_file}", file=sys.stderr
221
- )
223
+ print(f"Error: --meta-file not found: {args.meta_file}", file=sys.stderr)
222
224
  sys.exit(1)
223
225
  cfg = load_toml(args.meta_file)
224
226
  overrides = _deep_merge_overrides(overrides, extract_meta_overrides(cfg))
@@ -245,6 +247,7 @@ def main():
245
247
  or args.backend == "docling"
246
248
  ):
247
249
  from any2md._docling import has_docling, INSTALL_HINT_MSG
250
+
248
251
  if not has_docling():
249
252
  print(
250
253
  f" ERROR: Docling required for --high-fidelity / --ocr-figures / --save-images / --backend docling.\n"
@@ -268,6 +271,7 @@ def main():
268
271
  auto_id_type_code=auto_id_type_code,
269
272
  frontmatter_overrides=overrides or None,
270
273
  backend=args.backend,
274
+ arxiv_lookup=not args.no_arxiv_lookup,
271
275
  )
272
276
 
273
277
  # CLI-only output controls (not part of PipelineOptions).