any2md 1.0.5__tar.gz → 1.1.1__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 (161) hide show
  1. {any2md-1.0.5 → any2md-1.1.1}/.gitignore +0 -1
  2. {any2md-1.0.5 → any2md-1.1.1}/CHANGELOG.md +255 -6
  3. {any2md-1.0.5 → any2md-1.1.1}/CONTRIBUTING.md +18 -0
  4. {any2md-1.0.5 → any2md-1.1.1}/PKG-INFO +29 -15
  5. {any2md-1.0.5 → any2md-1.1.1}/README.md +16 -7
  6. {any2md-1.0.5 → any2md-1.1.1}/SECURITY.md +22 -0
  7. any2md-1.1.1/any2md/__init__.py +11 -0
  8. {any2md-1.0.5 → any2md-1.1.1}/any2md/_docling.py +1 -1
  9. any2md-1.1.1/any2md/_docling_cache.py +436 -0
  10. any2md-1.1.1/any2md/_http.py +199 -0
  11. any2md-1.1.1/any2md/_logging.py +61 -0
  12. {any2md-1.0.5 → any2md-1.1.1}/any2md/cli.py +22 -8
  13. {any2md-1.0.5 → any2md-1.1.1}/any2md/config.py +22 -3
  14. {any2md-1.0.5 → any2md-1.1.1}/any2md/converters/__init__.py +2 -2
  15. {any2md-1.0.5 → any2md-1.1.1}/any2md/converters/docx.py +54 -14
  16. {any2md-1.0.5 → any2md-1.1.1}/any2md/converters/html.py +43 -70
  17. {any2md-1.0.5 → any2md-1.1.1}/any2md/converters/pdf.py +46 -18
  18. {any2md-1.0.5 → any2md-1.1.1}/any2md/converters/txt.py +6 -7
  19. {any2md-1.0.5 → any2md-1.1.1}/any2md/frontmatter.py +65 -6
  20. {any2md-1.0.5 → any2md-1.1.1}/any2md/heuristics.py +21 -26
  21. {any2md-1.0.5 → any2md-1.1.1}/any2md/pipeline/cleanup.py +1 -1
  22. any2md-1.1.1/any2md/utils.py +161 -0
  23. {any2md-1.0.5 → any2md-1.1.1}/docs/architecture.md +13 -10
  24. {any2md-1.0.5 → any2md-1.1.1}/docs/cli-reference.md +1 -1
  25. {any2md-1.0.5 → any2md-1.1.1}/docs/output-format.md +19 -19
  26. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md +19 -19
  27. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/plans/2026-04-26-any2md-v1-phase4-polish-config-docs.md +11 -11
  28. any2md-1.1.1/docs/superpowers/plans/2026-04-27-v1.0.6-security-hardening.md +1867 -0
  29. any2md-1.1.1/docs/superpowers/plans/2026-05-04-v1.1.0-persistent-converter.md +2243 -0
  30. any2md-1.1.1/docs/superpowers/plans/2026-05-06-v1.1.1-security-hardening.md +2543 -0
  31. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/specs/2026-04-26-any2md-v1-design.md +25 -25
  32. any2md-1.1.1/docs/superpowers/specs/2026-04-27-v1.0.6-security-hardening-design.md +477 -0
  33. any2md-1.1.1/docs/superpowers/specs/2026-05-04-v1.1.0-persistent-converter-design.md +951 -0
  34. any2md-1.1.1/docs/superpowers/specs/2026-05-06-v1.1.1-security-design.md +553 -0
  35. {any2md-1.0.5 → any2md-1.1.1}/docs/troubleshooting.md +76 -0
  36. {any2md-1.0.5 → any2md-1.1.1}/docs/upgrading-from-0.7.md +9 -9
  37. {any2md-1.0.5 → any2md-1.1.1}/pyproject.toml +12 -7
  38. any2md-1.1.1/requirements.txt +81 -0
  39. any2md-1.1.1/scripts/bench_docling_cache.py +87 -0
  40. {any2md-1.0.5 → any2md-1.1.1}/scripts/validate-phase1.sh +1 -1
  41. any2md-1.1.1/tests/conftest.py +43 -0
  42. any2md-1.1.1/tests/integration/test_docling_persistence.py +105 -0
  43. any2md-1.1.1/tests/integration/test_html_trafilatura.py +105 -0
  44. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_txt.py +1 -1
  45. any2md-1.1.1/tests/unit/test_atomic_write.py +52 -0
  46. any2md-1.1.1/tests/unit/test_atomic_write_symlink_race.py +39 -0
  47. any2md-1.1.1/tests/unit/test_config_toml_warn.py +36 -0
  48. any2md-1.1.1/tests/unit/test_config_walkup.py +53 -0
  49. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_content_hash.py +1 -1
  50. any2md-1.1.1/tests/unit/test_docling_cache.py +703 -0
  51. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_frontmatter_compose.py +2 -2
  52. any2md-1.1.1/tests/unit/test_frontmatter_key_injection.py +59 -0
  53. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_heuristics.py +21 -27
  54. any2md-1.1.1/tests/unit/test_heuristics_wikipedia_host.py +22 -0
  55. any2md-1.1.1/tests/unit/test_http_ip_allowlist.py +68 -0
  56. any2md-1.1.1/tests/unit/test_http_safe.py +202 -0
  57. any2md-1.1.1/tests/unit/test_http_ssrf_pin.py +63 -0
  58. any2md-1.1.1/tests/unit/test_http_user_agent.py +10 -0
  59. any2md-1.1.1/tests/unit/test_lockfile_freshness.py +41 -0
  60. any2md-1.1.1/tests/unit/test_log_sanitizer.py +27 -0
  61. any2md-1.1.1/tests/unit/test_logging_module.py +85 -0
  62. any2md-1.1.1/tests/unit/test_meta_reserved_keys.py +37 -0
  63. any2md-1.1.1/tests/unit/test_pdf_image_symlink.py +54 -0
  64. any2md-1.1.1/tests/unit/test_safe_dir_name.py +25 -0
  65. any2md-1.1.1/tests/unit/test_security_error_strings.py +86 -0
  66. any2md-1.1.1/tests/unit/test_url_creds_scrubbed.py +76 -0
  67. any2md-1.1.1/tests/unit/test_workflow_permissions.py +44 -0
  68. any2md-1.1.1/tests/unit/test_zipbomb_guard.py +47 -0
  69. any2md-1.0.5/any2md/__init__.py +0 -3
  70. any2md-1.0.5/any2md/utils.py +0 -70
  71. any2md-1.0.5/requirements.txt +0 -7
  72. any2md-1.0.5/tests/conftest.py +0 -25
  73. any2md-1.0.5/tests/integration/test_html_trafilatura.py +0 -29
  74. {any2md-1.0.5 → any2md-1.1.1}/CODE_OF_CONDUCT.md +0 -0
  75. {any2md-1.0.5 → any2md-1.1.1}/LICENSE +0 -0
  76. {any2md-1.0.5 → any2md-1.1.1}/any2md/__main__.py +0 -0
  77. {any2md-1.0.5 → any2md-1.1.1}/any2md/pipeline/__init__.py +0 -0
  78. {any2md-1.0.5 → any2md-1.1.1}/any2md/pipeline/structured.py +0 -0
  79. {any2md-1.0.5 → any2md-1.1.1}/any2md/pipeline/text.py +0 -0
  80. {any2md-1.0.5 → any2md-1.1.1}/any2md/validators.py +0 -0
  81. {any2md-1.0.5 → any2md-1.1.1}/assets/logo-original.jpeg +0 -0
  82. {any2md-1.0.5 → any2md-1.1.1}/assets/logo.png +0 -0
  83. {any2md-1.0.5 → any2md-1.1.1}/assets/social-preview.jpg +0 -0
  84. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/plans/2026-04-26-any2md-v1-phase2-docling-backend.md +0 -0
  85. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/plans/2026-04-26-any2md-v1-phase3-figures-ocr-text-lane.md +0 -0
  86. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/plans/2026-04-26-any2md-v1.0.2-implementation.md +0 -0
  87. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/plans/2026-04-26-issue-17-toc-followups.md +0 -0
  88. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md +0 -0
  89. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/specs/2026-04-26-issue-17-toc-followups-design.md +0 -0
  90. {any2md-1.0.5 → any2md-1.1.1}/docs/superpowers/specs/2026-04-26-v1.0.3-empty-title-orphan-punct-design.md +0 -0
  91. {any2md-1.0.5 → any2md-1.1.1}/scripts/audit-outputs.py +0 -0
  92. {any2md-1.0.5 → any2md-1.1.1}/tests/__init__.py +0 -0
  93. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/__init__.py +0 -0
  94. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_backend.py +0 -0
  95. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_existing_flags.py +0 -0
  96. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_exit_codes.py +0 -0
  97. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_high_fidelity.py +0 -0
  98. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_meta.py +0 -0
  99. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_no_arxiv_lookup.py +0 -0
  100. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_ocr_save_images.py +0 -0
  101. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_quiet_verbose.py +0 -0
  102. {any2md-1.0.5 → any2md-1.1.1}/tests/cli/test_cli_strict.py +0 -0
  103. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/__init__.py +0 -0
  104. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/docs/ligatures_and_softhyphens.txt +0 -0
  105. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/docs/multi_column.pdf +0 -0
  106. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/docs/table_heavy.docx +0 -0
  107. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/docs/web_page.html +0 -0
  108. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/make_fixtures.py +0 -0
  109. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/snapshots/ligatures_and_softhyphens.md +0 -0
  110. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/snapshots/multi_column.docling.md +0 -0
  111. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/snapshots/multi_column.fallback.md +0 -0
  112. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/snapshots/table_heavy.docling.md +0 -0
  113. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/snapshots/table_heavy.fallback.md +0 -0
  114. {any2md-1.0.5 → any2md-1.1.1}/tests/fixtures/snapshots/web_page.md +0 -0
  115. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/__init__.py +0 -0
  116. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_docx_backend_override.py +0 -0
  117. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_docx_docling.py +0 -0
  118. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_docx_fallback_on_warn.py +0 -0
  119. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_docx_mammoth_fallback.py +0 -0
  120. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_pdf_backend_override.py +0 -0
  121. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_pdf_docling.py +0 -0
  122. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_pdf_pymupdf_fallback.py +0 -0
  123. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_save_images.py +0 -0
  124. {any2md-1.0.5 → any2md-1.1.1}/tests/integration/test_snapshots.py +0 -0
  125. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/__init__.py +0 -0
  126. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/__init__.py +0 -0
  127. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_footnote_markers.py +0 -0
  128. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_html_entities.py +0 -0
  129. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_html_entities_double_encoded.py +0 -0
  130. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_ligatures.py +0 -0
  131. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_nfc.py +0 -0
  132. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_quotes_dashes.py +0 -0
  133. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_soft_hyphens.py +0 -0
  134. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_validate.py +0 -0
  135. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_cleanup_whitespace.py +0 -0
  136. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_runner.py +0 -0
  137. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_strip_orphan_punctuation.py +0 -0
  138. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_structured_body_cleanup_stages.py +0 -0
  139. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_structured_compact_tables.py +0 -0
  140. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_structured_heading_hierarchy.py +0 -0
  141. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_structured_lift_figures.py +0 -0
  142. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_structured_normalize_cites.py +0 -0
  143. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_dedupe_paragraphs.py +0 -0
  144. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_dedupe_toc.py +0 -0
  145. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_dedupe_toc_table.py +0 -0
  146. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_dehyphenate.py +0 -0
  147. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_repair_line_wraps.py +0 -0
  148. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_restore_lists_code.py +0 -0
  149. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_strip_cover_artifacts.py +0 -0
  150. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_strip_headers_footers.py +0 -0
  151. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_strip_repeated_byline.py +0 -0
  152. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/pipeline/test_text_strip_web_fragments.py +0 -0
  153. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/scripts/__init__.py +0 -0
  154. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/scripts/test_audit_outputs.py +0 -0
  155. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_auto_id.py +0 -0
  156. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_config.py +0 -0
  157. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_docling_helper.py +0 -0
  158. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_frontmatter_helpers.py +0 -0
  159. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_meta_overrides.py +0 -0
  160. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_pdf_complexity.py +0 -0
  161. {any2md-1.0.5 → any2md-1.1.1}/tests/unit/test_validators.py +0 -0
@@ -43,4 +43,3 @@ logo/
43
43
  # ZERG / dev tooling
44
44
  .gsd/
45
45
  .zerg/
46
- .devcontainer/
@@ -4,6 +4,255 @@ 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.1.1] — 2026-05-DD
8
+
9
+ Security patch release. Closes 10 findings from a comprehensive scan
10
+ (bandit, snyk-code, gitleaks, pip-audit, trivy, manual review) plus
11
+ a 5-round adversarial review of the implementation plan.
12
+
13
+ ### Security
14
+
15
+ - **SSRF intra-hop DNS-rebind**: `safe_fetch` now resolves the
16
+ hostname once, validates the IP against an allowlist, and passes
17
+ the validated IP through to `socket.create_connection` via a
18
+ custom `HTTPSConnection`. Defeats attacks that return a public IP
19
+ to a validator and a private IP (e.g., `169.254.169.254`) to the
20
+ connector. `ProxyHandler({})` explicitly suppresses
21
+ `HTTP_PROXY`/`HTTPS_PROXY` env vars that would otherwise route
22
+ traffic past the validator.
23
+ - **IP allowlist**: switched from a four-flag denylist
24
+ (`is_private | is_reserved | is_loopback | is_link_local`) to an
25
+ allowlist (`is_global and not is_multicast and not is_unspecified`).
26
+ **Breaking** for users on multicast / CGNAT / IETF-reserved
27
+ networks fetching same-network resources; pre-fetch with `curl`
28
+ and pass `--from-file` as a workaround.
29
+ - **Frontmatter override-key validation**: keys must now match
30
+ `^[A-Za-z_][A-Za-z0-9_-]*$`. Defends against newline-injection in
31
+ TOML basic-string keys that produced split YAML lines.
32
+ **Breaking** for TOML configs with quoted keys containing
33
+ whitespace or punctuation.
34
+ - **URL credentials and sensitive query parameters** in `userinfo`
35
+ and matching `(?i)(token|api[_-]?key|access[_-]?token|bearer|signature|sig|password|secret|aws[_-]?session[_-]?token)$`
36
+ are scrubbed before any logging, persistence, or filename
37
+ derivation. IPv6 host brackets preserved across the scrub.
38
+ - **Docling image saves** use `O_EXCL|O_NOFOLLOW` to refuse symlink
39
+ TOCTOU.
40
+ - **`atomic_write_text`** tightens the symlink-check window via
41
+ `os.lstat` immediately before `os.replace`. Parent-dir hardening
42
+ deferred to v1.2 per the operator-trusted threat model.
43
+ - **Stderr/stdout sanitizer** extended with Unicode bidi
44
+ (U+202A–E, U+2066–9), zero-width chars (U+200B–F, U+FEFF), and
45
+ the BOM. New `any2md._logging` module; `_sanitize_log_text`
46
+ re-exported from `any2md.converters.docx` for one release.
47
+ - **`publish.yml` workflow**: top-level `permissions: contents: read`;
48
+ both publish jobs flip to `attestations: true` for PEP 740 sigstore
49
+ provenance.
50
+
51
+ ### Fixed
52
+
53
+ - `wikipedia.org` heuristic host check no longer matches
54
+ `evilwikipedia.org`. Pure correctness fix; impact was incorrect
55
+ text formatting only, not a security boundary.
56
+ - `_USER_AGENT` no longer hardcoded to `any2md/1.0.6`; derives from
57
+ `__version__`.
58
+
59
+ ### Changed
60
+
61
+ - `requirements.txt` is now a generated artifact; canonical source
62
+ is `.devcontainer/requirements.lock` with hashes
63
+ (`--require-hashes` enforced in `.devcontainer/devcontainer.json`).
64
+ See `CONTRIBUTING.md` for the dep-update workflow.
65
+ - `load_toml()` now warns to stderr on parse error rather than
66
+ silently returning `{}`.
67
+ - `.devcontainer/` is now tracked in git (was previously ignored)
68
+ so the security-critical Codespace bootstrap config is
69
+ version-controlled.
70
+
71
+ ## [1.1.0] — 2026-05-04
72
+
73
+ Performance and library-mode hygiene release.
74
+
75
+ ### Added
76
+
77
+ - `any2md.release_models()` — imperative escape hatch to free
78
+ cached Docling `DocumentConverter` instances. Stability:
79
+ experimental for v1.1.0.
80
+ - `any2md.docling_session()` — contextmanager (preferred shape
81
+ for library use). Stability: experimental for v1.1.0.
82
+ - `ANY2MD_DOCLING_CACHE=0` env-var to disable the persistent
83
+ converter cache (per-call construction, equivalent to v1.0.x
84
+ behavior).
85
+ - `__all__` declared on `any2md/__init__.py` (formalizes the
86
+ public surface).
87
+ - New benchmark `scripts/bench_docling_cache.py` for maintainer
88
+ pre-release verification.
89
+
90
+ ### Changed
91
+
92
+ - `DocumentConverter` is now constructed once per process and
93
+ reused across all files in a batch (was: once per file). New
94
+ one-time stderr line `Loading Docling models (one-time)...`
95
+ printed on first PDF/DOCX in non-quiet TTY runs.
96
+ - `.github/workflows/ci.yml` now runs `pytest tests/unit` on each
97
+ push and PR (was: lint + import smoke + pip-audit only).
98
+
99
+ ### Performance
100
+
101
+ - Measured: CPU first-call ~2.47s (model load), warm calls ~0.87s
102
+ (vs prior ~2.47s per file in v1.0.x). Mac MPS first-call ~2.77s,
103
+ warm ~0.14s. RSS floor +535 MB sustained for the process lifetime
104
+ (see `docs/troubleshooting.md` for `release_models()` mitigation).
105
+
106
+ ### Migration
107
+
108
+ - None required for CLI users. The `Loading Docling models...`
109
+ stderr line is the only user-visible default-output change.
110
+ - Library/embedder users holding any2md across long-running
111
+ processes should call `any2md.release_models()` or use
112
+ `with any2md.docling_session():` to free model state between
113
+ workloads.
114
+ - Test/instrumentation code that monkey-patches
115
+ `docling.document_converter.DocumentConverter` AFTER any2md's
116
+ first call will see no effect on subsequent calls (cache returns
117
+ the pre-patch instance). Workaround: call `release_models()`
118
+ after applying the patch, or set `ANY2MD_DOCLING_CACHE=0` in the
119
+ test environment.
120
+
121
+ ### Stability notice
122
+
123
+ The new public APIs (`release_models`, `docling_session`) are
124
+ marked experimental for v1.1.0; their shape may change in
125
+ subsequent v1.x releases without a major bump until promoted to
126
+ stable in a later release.
127
+
128
+ ### Known limitations
129
+
130
+ - HF Hub outages during cold-start now cause N retries instead of
131
+ one silent fallback (the v1.0.x failure-cache behavior was
132
+ rejected on quality grounds). Bounded retry-with-backoff
133
+ deferred to a future hardening PR.
134
+ - Forking a process (`multiprocessing` with `fork` start method)
135
+ after the first `convert()` call inherits torch/CUDA state
136
+ unsafely regardless of cache. Use
137
+ `multiprocessing.set_start_method("spawn")` or call
138
+ `release_models()` before fork.
139
+ - `os.register_at_fork` is POSIX-only; on Windows the fork-reset
140
+ path is dead code (no fork on Windows). Cache works normally
141
+ on Windows.
142
+
143
+ ## [1.0.7] — 2026-04-29
144
+
145
+ Branding/rebrand release. The frontmatter contract that any2md emits is
146
+ now described as "SAGE-compatible" (Security Analysis and Guidance
147
+ Exchange) — the upstream specification was renamed from SSRM (Structured
148
+ Security Reasoning Markdown) to SAGE. No functional or schema changes:
149
+ field names, ordering, controlled vocabularies, and `content_hash`
150
+ derivation are byte-identical to v1.0.6. Existing v1.0.6 outputs remain
151
+ valid SAGE-compatible documents without modification.
152
+
153
+ ### Changed
154
+
155
+ - README, CHANGELOG, and all of `docs/` updated to reference SAGE /
156
+ Security Analysis and Guidance Exchange.
157
+ - `--auto-id` CLI help text now reads "SAGE-conformant document_id"
158
+ (previously "SSRM-conformant"). Behavior unchanged.
159
+ - Module/function docstrings in `any2md/frontmatter.py`,
160
+ `any2md/converters/txt.py`, and `any2md/pipeline/cleanup.py` updated
161
+ to reference SAGE §3 / §5.1 (same section numbers as the upstream
162
+ SSRM v1.0-RC1 spec).
163
+ - Two test functions renamed for naming consistency
164
+ (`test_txt_end_to_end_writes_sage_compat_output`,
165
+ `test_compose_emits_required_sage_fields`). Pytest auto-discovery is
166
+ unaffected; assertions unchanged.
167
+
168
+ ### Migration
169
+
170
+ None required. Public API, CLI flags, frontmatter shape, and
171
+ `content_hash` outputs are all preserved bit-for-bit.
172
+
173
+ ## [1.0.6] — 2026-04-27
174
+
175
+ Security hardening release. Closes 8 actionable findings from the
176
+ 2026-04-27 multi-tool audit (semgrep + trivy + gitleaks + bandit +
177
+ pip-audit + manual review by the security-engineer agent + Opus 4.7).
178
+ No public API changes; behavior unchanged for legitimate inputs.
179
+
180
+ ### Security
181
+
182
+ - **F1 (HIGH)** SSRF: replaced `trafilatura.fetch_url` and the bare
183
+ `urllib.request.urlopen` paths with a new `any2md/_http.py` module
184
+ that does manual redirect walking with per-hop revalidation. Defends
185
+ against DNS-rebinding and redirect-based bypass of the IP-class
186
+ check. New `validate_url` rejects non-http(s) schemes and
187
+ private/loopback/link-local/reserved targets. 20 MB body cap, 3-hop
188
+ redirect cap, redirect-loop detection. Applied to `convert_url`,
189
+ `_http_last_modified`, and `arxiv_lookup`.
190
+ - **F5 (MED)** `--meta` integrity: `content_hash`, `extracted_via`,
191
+ `source_file`, `lane`, `token_estimate`, `recommended_chunk_level`
192
+ are now **reserved** and silently filtered from `--meta` /
193
+ `--meta-file` / `.any2md.toml` overrides with a stderr `WARN` per
194
+ drop. Defense-in-depth filter also runs inside `frontmatter.compose`.
195
+ - **F3 (MED)** TOML auto-discovery now stops at the first ancestor
196
+ containing `.git` / `pyproject.toml` / `setup.py` / `setup.cfg` /
197
+ `package.json` / `.any2md.toml.boundary`. New `--no-config` flag
198
+ fully disables discovery.
199
+ - **F4 (MED)** DOCX zip-bomb: `_safe_zip_open` enforces a 1 MB cap on
200
+ the declared uncompressed size of `core.xml` and `app.xml` before
201
+ parsing. Oversized members raise `ValueError` → clean per-file FAIL.
202
+ - **F-CVE (MED)** Dependency floors bumped: `lxml>=6.1.0,<7`
203
+ (CVE-2026-41066), `pillow>=12.2.0,<13` (CVE-2026-25990,
204
+ CVE-2026-40192), `urllib3>=2.6.3,<3` (CVE-2026-21441). All ranges
205
+ acquire upper bounds (compat envelope). `requirements.txt` becomes
206
+ a development/CI lockfile with exact pins.
207
+ - **F6 (LOW)** Atomic writes: new `any2md.utils.atomic_write_text`
208
+ writes via `tempfile.mkstemp` + `os.replace`. Refuses to overwrite
209
+ a pre-existing symlink at the output path. Wired through
210
+ `pdf/docx/html/txt` converters; PDF image extraction adds a symlink
211
+ check before `pil_image.save`.
212
+ - **F2 (LOW)** Control-char sanitizer strips C0/C1 controls (preserves
213
+ `\t`, `\n`) from captured Docling `msword_backend` warnings before
214
+ forwarding them to `collected_warnings()` and stderr. Defends
215
+ terminals from ANSI-escape spoofing.
216
+ - **F11 (LOW)** PDF image-dir name now goes through `safe_dir_name`
217
+ (alphanumeric/`_`/`-` only). Closes a `Path.stem == ".."` corner
218
+ case that co-mingled images with markdown outputs.
219
+ - **XXE defense-in-depth**: migrated `xml.etree.ElementTree` →
220
+ `defusedxml.ElementTree` in `docx.py` and `heuristics.py`. Stdlib
221
+ was already safe in Python 3.7+; this silences bandit B405/B314
222
+ permanently and protects against future stdlib regressions.
223
+
224
+ ### Added
225
+
226
+ - New module `any2md/_http.py` (`validate_url`, `safe_fetch`).
227
+ - New CLI flag `--no-config`.
228
+ - New helpers `any2md.utils.atomic_write_text` and `safe_dir_name`.
229
+ - New runtime dependency: `defusedxml>=0.7.1,<1`.
230
+
231
+ ### Tests
232
+
233
+ - 7 new unit-test files (40 new tests): `test_http_safe.py`,
234
+ `test_meta_reserved_keys.py`, `test_config_walkup.py`,
235
+ `test_zipbomb_guard.py`, `test_atomic_write.py`,
236
+ `test_safe_dir_name.py`, `test_log_sanitizer.py`.
237
+ - Existing 290 tests continue to pass.
238
+ - Bandit audit went from **9 findings** (3 medium, 6 low) to **1 low**
239
+ (legitimate `try/except/pass` in arxiv `_warn`).
240
+ - 5-DOCX regression run reproduces v1.0.5 word counts exactly
241
+ (978/6726/15566/7811/1477).
242
+
243
+ ### Audit summary
244
+
245
+ | ID | Title | Severity |
246
+ |----|-------|----------|
247
+ | F1 | SSRF: DNS rebind + redirect bypass | High |
248
+ | F5 | `--meta` clobbers reserved frontmatter | Medium |
249
+ | F3 | TOML discovery walks above project root | Medium |
250
+ | F4 | DOCX zip-bomb amplification | Medium |
251
+ | F-CVE | Transitive deps with known CVEs | Medium |
252
+ | F6 | Symlink-following on output writes | Low |
253
+ | F2 | Control-char passthrough in Docling logs | Low |
254
+ | F11 | PDF stem `..` corner case | Low |
255
+
7
256
  ## [1.0.5] — 2026-04-27
8
257
 
9
258
  Patch release. Recovers DOCX content that Docling's msword backend
@@ -235,7 +484,7 @@ Patch release. Adds an explicit backend-selection CLI flag.
235
484
  First stable release of any2md v1.0. Validated end-to-end against
236
485
  real-world documents (a 164-page PDF technical standard via Docling,
237
486
  a multi-page academic DOCX, and a Wikipedia article via trafilatura).
238
- The output contract — SSRM-compatible frontmatter, deterministic
487
+ The output contract — SAGE-compatible frontmatter, deterministic
239
488
  `content_hash`, NFC + LF body normalization — is now stable. Downstream
240
489
  consumers can rely on this shape.
241
490
 
@@ -257,7 +506,7 @@ Phase 4: configuration, polish, and the v1.0 documentation set.
257
506
  - `--meta-file PATH` flag plus auto-discovery of `.any2md.toml` (walks up
258
507
  from cwd). The `[meta]` table supplies frontmatter overrides; the
259
508
  `[document_id]` table supplies `--auto-id` prefix and type code.
260
- - `--auto-id` flag — generates an SSRM-conformant `document_id` as
509
+ - `--auto-id` flag — generates an SAGE-conformant `document_id` as
261
510
  `{PREFIX}-{YYYY}-{TYPE}-{SHA8}`. Defaults to `LOCAL`/`DOC`; override via
262
511
  the `[document_id]` table in `.any2md.toml`.
263
512
  - `--strict` flag — promotes pipeline validation warnings (heading
@@ -276,7 +525,7 @@ Phase 4: configuration, polish, and the v1.0 documentation set.
276
525
  - Comprehensive documentation set:
277
526
  - `README.md` rewritten with educational tone — every command preceded
278
527
  by why you'd run it and followed by what you'll see.
279
- - `docs/output-format.md` — SSRM-compatible field reference and the
528
+ - `docs/output-format.md` — SAGE-compatible field reference and the
280
529
  `content_hash` recomputation recipe.
281
530
  - `docs/cli-reference.md` — flag-by-flag reference with use cases and a
282
531
  worked-example matrix.
@@ -379,14 +628,14 @@ any2md transparently falls back to pymupdf4llm (PDF) and mammoth (DOCX).
379
628
 
380
629
  First prerelease of any2md v1.0. Phase 1 of 5: foundation only — no Docling
381
630
  backend yet, no new CLI flags. Output frontmatter has been rewritten to be
382
- SSRM-compatible (Structured Security Reasoning Markdown) — this is a
631
+ SAGE-compatible (Security Analysis and Guidance Exchange) — this is a
383
632
  breaking change for downstream consumers parsing v0.7 output. See
384
633
  `docs/superpowers/specs/2026-04-26-any2md-v1-design.md` for the full
385
634
  v1.0 design and `docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md`
386
635
  for this phase's task plan.
387
636
 
388
637
  ### Added
389
- - New `any2md/frontmatter.py` module — SSRM-compatible YAML emitter with the
638
+ - New `any2md/frontmatter.py` module — SAGE-compatible YAML emitter with the
390
639
  `SourceMeta` dataclass, deterministic `compute_content_hash`,
391
640
  `estimate_tokens`, `recommend_chunk_level`, `extract_abstract`, and
392
641
  `derive_title` helpers, plus the central `compose()` function (the only
@@ -412,7 +661,7 @@ for this phase's task plan.
412
661
  `reportlab`, `ruff`.
413
662
 
414
663
  ### Changed
415
- - **BREAKING:** Output frontmatter shape is now SSRM-compatible. New required
664
+ - **BREAKING:** Output frontmatter shape is now SAGE-compatible. New required
416
665
  fields: `document_id` (empty string by default — opt-in via future
417
666
  `--auto-id`), `version`, `date`, `status: "draft"`, `document_type` (empty
418
667
  for non-security documents), `content_domain` (empty array),
@@ -131,6 +131,24 @@ The flow:
131
131
  Don't tag and release directly without the PR — CI gates exist for a
132
132
  reason.
133
133
 
134
+ ## Updating dependencies
135
+
136
+ Dependencies are declared in `pyproject.toml` and locked into both:
137
+
138
+ - `.devcontainer/requirements.lock` — hash-locked, used by Codespace `postCreateCommand` (`pip install --require-hashes`).
139
+ - `requirements.txt` — non-hash form, kept for backwards compatibility with `pip install -r requirements.txt` consumers.
140
+
141
+ To add or update a dep:
142
+
143
+ 1. Edit `pyproject.toml` `[project] dependencies` (or the relevant `[project.optional-dependencies]` group).
144
+ 2. Regenerate both lockfiles in a venv with `pip-tools` installed:
145
+
146
+ pip-compile pyproject.toml --generate-hashes --resolver=backtracking \
147
+ -o .devcontainer/requirements.lock
148
+ pip-compile pyproject.toml --resolver=backtracking -o requirements.txt
149
+
150
+ 3. Commit all three files together.
151
+
134
152
  ## Pull request process
135
153
 
136
154
  - Branch off `main` (or off a phase branch like `v1.0` if one is open for
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: any2md
3
- Version: 1.0.5
3
+ Version: 1.1.1
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
@@ -12,16 +12,21 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Topic :: Text Processing :: Markup :: Markdown
14
14
  Requires-Python: >=3.10
15
- Requires-Dist: beautifulsoup4>=4.12.0
16
- Requires-Dist: lxml>=5.0.0
17
- Requires-Dist: mammoth>=1.6.0
18
- Requires-Dist: markdownify>=0.13.0
19
- Requires-Dist: pymupdf4llm>=0.0.17
20
- Requires-Dist: pymupdf>=1.24.0
21
- Requires-Dist: trafilatura>=1.12.0
15
+ Requires-Dist: beautifulsoup4<5,>=4.12.0
16
+ Requires-Dist: defusedxml<1,>=0.7.1
17
+ Requires-Dist: lxml<7,>=6.1.0
18
+ Requires-Dist: mammoth<2,>=1.6.0
19
+ Requires-Dist: markdownify<2,>=0.13.0
20
+ Requires-Dist: pillow<13,>=12.2.0
21
+ Requires-Dist: pymupdf4llm<1,>=0.0.17
22
+ Requires-Dist: pymupdf<2,>=1.24.0
23
+ Requires-Dist: tomli>=2.0.0; python_version < '3.11'
24
+ Requires-Dist: trafilatura<3,>=1.12.0
25
+ Requires-Dist: urllib3<3,>=2.6.3
22
26
  Provides-Extra: dev
23
27
  Requires-Dist: pytest-snapshot>=0.9.0; extra == 'dev'
24
28
  Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
25
30
  Requires-Dist: reportlab>=4.0.0; extra == 'dev'
26
31
  Requires-Dist: ruff>=0.6.0; extra == 'dev'
27
32
  Provides-Extra: high-fidelity
@@ -49,7 +54,7 @@ The full design rationale lives in [docs/superpowers/specs/2026-04-26-v1.0.3-emp
49
54
 
50
55
  ## What this is
51
56
 
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.
57
+ any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SAGE-compatible (Security Analysis and Guidance Exchange — 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.
53
58
 
54
59
  ## Quick start
55
60
 
@@ -141,8 +146,8 @@ document_id: "" # empty unless you pass --aut
141
146
  version: "1" # "1" for source files without an embedded version
142
147
  date: "2026-03-15" # docx core props "modified" → ISO-8601
143
148
  status: "draft" # always "draft" for converted documents
144
- document_type: "" # SSRM controlled vocab — empty for non-security docs
145
- content_domain: [] # SSRM controlled vocab — empty array when unknown
149
+ document_type: "" # SAGE controlled vocab — empty for non-security docs
150
+ content_domain: [] # SAGE controlled vocab — empty array when unknown
146
151
  authors: # extracted from docProps/core.xml dc:creator
147
152
  - "Rock Lambros"
148
153
  organization: "" # docProps/app.xml Company; empty when absent
@@ -163,7 +168,7 @@ produced_by: "Microsoft® Word for Microsoft 365" # software that produced the
163
168
  ---
164
169
  ```
165
170
 
166
- Fields under `# any2md extension field` are retained from v0.7 for traceability and observability — they're not part of the SSRM contract proper, but they live in the same frontmatter block. See [docs/output-format.md](docs/output-format.md) for the full field reference.
171
+ Fields under `# any2md extension field` are retained from v0.7 for traceability and observability — they're not part of the SAGE contract proper, but they live in the same frontmatter block. See [docs/output-format.md](docs/output-format.md) for the full field reference.
167
172
 
168
173
  ### Before / after
169
174
 
@@ -357,9 +362,18 @@ any2md -f -r ./corpus/
357
362
 
358
363
  You'll see the OK lines as if the conversion were fresh; previously-written outputs are overwritten.
359
364
 
365
+ > **v1.1.0+:** when processing PDFs or DOCX files via the Docling
366
+ > backend, the converter (and its loaded model weights) is constructed
367
+ > once on the first PDF/DOCX in an invocation and reused for all
368
+ > subsequent files; subsequent files extract in ~0.5–1s rather than
369
+ > re-paying the ~2.5s cold-load tax. Library users embedding any2md
370
+ > in long-running processes should call `any2md.release_models()` or
371
+ > use `with any2md.docling_session():` — see
372
+ > `docs/troubleshooting.md`.
373
+
360
374
  ## The output format
361
375
 
362
- Every file converted by any2md has the same frontmatter shape, regardless of source format. The shape is **SSRM-compatible** — it matches the field names, types, and ordering of the Structured Security Reasoning Markdown specification, but values aren't required to match SSRM's controlled vocabularies (most converted documents aren't security research). Fields that require a controlled vocabulary (`document_type`, `content_domain`, `tlp`) are emitted empty unless you supply them via `--meta` or `.any2md.toml`.
376
+ Every file converted by any2md has the same frontmatter shape, regardless of source format. The shape is **SAGE-compatible** — it matches the field names, types, and ordering of the Security Analysis and Guidance Exchange specification, but values aren't required to match SAGE's controlled vocabularies (most converted documents aren't security research). Fields that require a controlled vocabulary (`document_type`, `content_domain`, `tlp`) are emitted empty unless you supply them via `--meta` or `.any2md.toml`.
363
377
 
364
378
  ### Field auto-fill summary
365
379
 
@@ -404,7 +418,7 @@ You'll see the OK line as usual. The frontmatter of `Text/paper.md` will have `o
404
418
 
405
419
  ### `.any2md.toml`
406
420
 
407
- You'd use this when you have a stable set of frontmatter defaults that should apply to every conversion in a project. The file is auto-discovered by walking up from the current working directory, so you can drop one at the root of your corpus repo and forget about it. Worked example for a security-research org producing SSRM-conforming outputs:
421
+ You'd use this when you have a stable set of frontmatter defaults that should apply to every conversion in a project. The file is auto-discovered by walking up from the current working directory, so you can drop one at the root of your corpus repo and forget about it. Worked example for a security-research org producing SAGE-conforming outputs:
408
422
 
409
423
  ```toml
410
424
  [meta]
@@ -558,7 +572,7 @@ The two-lane design exists because Docling output is layout-correct (running agg
558
572
 
559
573
  ## Migrating from v0.7
560
574
 
561
- v1.0 emits SSRM-compatible frontmatter — that's the one intended breaking change. v0.7's small frontmatter (`title`, `source_file`, `pages`, `type`, `word_count`) becomes a fuller block with `document_id`, `version`, `date`, `status`, `content_hash`, `token_estimate`, `authors`, and several other fields. The v0.7 fields are retained as any2md extension fields in the same block, so traceability is preserved, but downstream parsers that expected the old fixed shape will need to update. The body is now NFC-normalized with LF line endings, which means `content_hash` is reproducible. If you need bit-for-bit v0.7 output, pin `any2md==0.7.0`. The full field-by-field migration guide and behavior-change list lives in [docs/upgrading-from-0.7.md](docs/upgrading-from-0.7.md).
575
+ v1.0 emits SAGE-compatible frontmatter — that's the one intended breaking change. v0.7's small frontmatter (`title`, `source_file`, `pages`, `type`, `word_count`) becomes a fuller block with `document_id`, `version`, `date`, `status`, `content_hash`, `token_estimate`, `authors`, and several other fields. The v0.7 fields are retained as any2md extension fields in the same block, so traceability is preserved, but downstream parsers that expected the old fixed shape will need to update. The body is now NFC-normalized with LF line endings, which means `content_hash` is reproducible. If you need bit-for-bit v0.7 output, pin `any2md==0.7.0`. The full field-by-field migration guide and behavior-change list lives in [docs/upgrading-from-0.7.md](docs/upgrading-from-0.7.md).
562
576
 
563
577
  ## Security
564
578
 
@@ -19,7 +19,7 @@ The full design rationale lives in [docs/superpowers/specs/2026-04-26-v1.0.3-emp
19
19
 
20
20
  ## What this is
21
21
 
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.
22
+ any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SAGE-compatible (Security Analysis and Guidance Exchange — 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.
23
23
 
24
24
  ## Quick start
25
25
 
@@ -111,8 +111,8 @@ document_id: "" # empty unless you pass --aut
111
111
  version: "1" # "1" for source files without an embedded version
112
112
  date: "2026-03-15" # docx core props "modified" → ISO-8601
113
113
  status: "draft" # always "draft" for converted documents
114
- document_type: "" # SSRM controlled vocab — empty for non-security docs
115
- content_domain: [] # SSRM controlled vocab — empty array when unknown
114
+ document_type: "" # SAGE controlled vocab — empty for non-security docs
115
+ content_domain: [] # SAGE controlled vocab — empty array when unknown
116
116
  authors: # extracted from docProps/core.xml dc:creator
117
117
  - "Rock Lambros"
118
118
  organization: "" # docProps/app.xml Company; empty when absent
@@ -133,7 +133,7 @@ produced_by: "Microsoft® Word for Microsoft 365" # software that produced the
133
133
  ---
134
134
  ```
135
135
 
136
- Fields under `# any2md extension field` are retained from v0.7 for traceability and observability — they're not part of the SSRM contract proper, but they live in the same frontmatter block. See [docs/output-format.md](docs/output-format.md) for the full field reference.
136
+ Fields under `# any2md extension field` are retained from v0.7 for traceability and observability — they're not part of the SAGE contract proper, but they live in the same frontmatter block. See [docs/output-format.md](docs/output-format.md) for the full field reference.
137
137
 
138
138
  ### Before / after
139
139
 
@@ -327,9 +327,18 @@ any2md -f -r ./corpus/
327
327
 
328
328
  You'll see the OK lines as if the conversion were fresh; previously-written outputs are overwritten.
329
329
 
330
+ > **v1.1.0+:** when processing PDFs or DOCX files via the Docling
331
+ > backend, the converter (and its loaded model weights) is constructed
332
+ > once on the first PDF/DOCX in an invocation and reused for all
333
+ > subsequent files; subsequent files extract in ~0.5–1s rather than
334
+ > re-paying the ~2.5s cold-load tax. Library users embedding any2md
335
+ > in long-running processes should call `any2md.release_models()` or
336
+ > use `with any2md.docling_session():` — see
337
+ > `docs/troubleshooting.md`.
338
+
330
339
  ## The output format
331
340
 
332
- Every file converted by any2md has the same frontmatter shape, regardless of source format. The shape is **SSRM-compatible** — it matches the field names, types, and ordering of the Structured Security Reasoning Markdown specification, but values aren't required to match SSRM's controlled vocabularies (most converted documents aren't security research). Fields that require a controlled vocabulary (`document_type`, `content_domain`, `tlp`) are emitted empty unless you supply them via `--meta` or `.any2md.toml`.
341
+ Every file converted by any2md has the same frontmatter shape, regardless of source format. The shape is **SAGE-compatible** — it matches the field names, types, and ordering of the Security Analysis and Guidance Exchange specification, but values aren't required to match SAGE's controlled vocabularies (most converted documents aren't security research). Fields that require a controlled vocabulary (`document_type`, `content_domain`, `tlp`) are emitted empty unless you supply them via `--meta` or `.any2md.toml`.
333
342
 
334
343
  ### Field auto-fill summary
335
344
 
@@ -374,7 +383,7 @@ You'll see the OK line as usual. The frontmatter of `Text/paper.md` will have `o
374
383
 
375
384
  ### `.any2md.toml`
376
385
 
377
- You'd use this when you have a stable set of frontmatter defaults that should apply to every conversion in a project. The file is auto-discovered by walking up from the current working directory, so you can drop one at the root of your corpus repo and forget about it. Worked example for a security-research org producing SSRM-conforming outputs:
386
+ You'd use this when you have a stable set of frontmatter defaults that should apply to every conversion in a project. The file is auto-discovered by walking up from the current working directory, so you can drop one at the root of your corpus repo and forget about it. Worked example for a security-research org producing SAGE-conforming outputs:
378
387
 
379
388
  ```toml
380
389
  [meta]
@@ -528,7 +537,7 @@ The two-lane design exists because Docling output is layout-correct (running agg
528
537
 
529
538
  ## Migrating from v0.7
530
539
 
531
- v1.0 emits SSRM-compatible frontmatter — that's the one intended breaking change. v0.7's small frontmatter (`title`, `source_file`, `pages`, `type`, `word_count`) becomes a fuller block with `document_id`, `version`, `date`, `status`, `content_hash`, `token_estimate`, `authors`, and several other fields. The v0.7 fields are retained as any2md extension fields in the same block, so traceability is preserved, but downstream parsers that expected the old fixed shape will need to update. The body is now NFC-normalized with LF line endings, which means `content_hash` is reproducible. If you need bit-for-bit v0.7 output, pin `any2md==0.7.0`. The full field-by-field migration guide and behavior-change list lives in [docs/upgrading-from-0.7.md](docs/upgrading-from-0.7.md).
540
+ v1.0 emits SAGE-compatible frontmatter — that's the one intended breaking change. v0.7's small frontmatter (`title`, `source_file`, `pages`, `type`, `word_count`) becomes a fuller block with `document_id`, `version`, `date`, `status`, `content_hash`, `token_estimate`, `authors`, and several other fields. The v0.7 fields are retained as any2md extension fields in the same block, so traceability is preserved, but downstream parsers that expected the old fixed shape will need to update. The body is now NFC-normalized with LF line endings, which means `content_hash` is reproducible. If you need bit-for-bit v0.7 output, pin `any2md==0.7.0`. The full field-by-field migration guide and behavior-change list lives in [docs/upgrading-from-0.7.md](docs/upgrading-from-0.7.md).
532
541
 
533
542
  ## Security
534
543
 
@@ -81,3 +81,25 @@ If you operate any2md in a hardened environment:
81
81
 
82
82
  No publicly disclosed vulnerabilities to date. Future advisories will
83
83
  appear at https://github.com/rocklambros/any2md/security/advisories.
84
+
85
+ ## Trust boundaries
86
+
87
+ `any2md` is an operator-trusted CLI distributed via PyPI for desktop and CI use.
88
+
89
+ **Trusted:** the operator, their filesystem, their `--output-dir`, and the Python interpreter they invoke. CLI flags and `--meta key=val` arguments are operator-controlled.
90
+
91
+ **Untrusted:**
92
+ - URLs passed to `any2md` (may resolve to internal IPs, redirect, or carry credentials in userinfo/query).
93
+ - Documents (PDF/DOCX/HTML) passed for conversion (may embed crafted images, control characters, malicious metadata).
94
+ - `.any2md.toml` config files discovered via cwd-walking (an attacker who can drop a file in any ancestor directory can inject frontmatter overrides).
95
+ - HTTP responses from URLs (may carry hostile redirects, oversized bodies, control characters in headers).
96
+
97
+ **Out of scope:** multi-tenant service hosting; running `any2md` against attacker-supplied output directories; supply-chain compromise of pinned dependencies; protection against an operator who has chosen `--output-dir` pointing at a shared/symlinked directory.
98
+
99
+ ## Dependency-drift policy
100
+
101
+ If `pip-audit` reports a NEW HIGH/CRITICAL vulnerability between baseline and release-cut, the policy is:
102
+
103
+ 1. Bump the dep within its compatible range (regenerate `.devcontainer/requirements.lock` and `requirements.txt`) and re-cut the rc.
104
+ 2. If no compatible fix exists, document in CHANGELOG `## Known issues` and ship only if the codepath is mitigated by other code (e.g., XXE blocked by `defusedxml`).
105
+ 3. If neither is possible, hold the release until upstream lands a fix.
@@ -0,0 +1,11 @@
1
+ """any2md — convert documents to LLM-friendly Markdown."""
2
+
3
+ from any2md._docling_cache import docling_session, release_models
4
+
5
+ __version__ = "1.1.1"
6
+
7
+ __all__ = [
8
+ "__version__",
9
+ "docling_session",
10
+ "release_models",
11
+ ]
@@ -33,5 +33,5 @@ def install_hint() -> None:
33
33
  global _hint_emitted
34
34
  if _hint_emitted:
35
35
  return
36
- print(f" WARN: {INSTALL_HINT_MSG}", file=sys.stderr)
36
+ print(f" WARN: {INSTALL_HINT_MSG}", file=sys.stderr) # log-safe: literal
37
37
  _hint_emitted = True