any2md 0.6.0__tar.gz → 1.0.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 (108) hide show
  1. {any2md-0.6.0 → any2md-1.0.1}/.gitignore +5 -0
  2. any2md-1.0.1/CHANGELOG.md +255 -0
  3. any2md-1.0.1/CONTRIBUTING.md +148 -0
  4. any2md-1.0.1/PKG-INFO +529 -0
  5. any2md-1.0.1/README.md +499 -0
  6. {any2md-0.6.0 → any2md-1.0.1}/any2md/__init__.py +1 -1
  7. any2md-1.0.1/any2md/_docling.py +37 -0
  8. any2md-1.0.1/any2md/cli.py +419 -0
  9. any2md-1.0.1/any2md/config.py +81 -0
  10. any2md-1.0.1/any2md/converters/__init__.py +86 -0
  11. any2md-1.0.1/any2md/converters/docx.py +185 -0
  12. any2md-1.0.1/any2md/converters/html.py +261 -0
  13. any2md-1.0.1/any2md/converters/pdf.py +275 -0
  14. {any2md-0.6.0 → any2md-1.0.1}/any2md/converters/txt.py +55 -78
  15. any2md-1.0.1/any2md/frontmatter.py +304 -0
  16. any2md-1.0.1/any2md/pipeline/__init__.py +80 -0
  17. any2md-1.0.1/any2md/pipeline/cleanup.py +129 -0
  18. any2md-1.0.1/any2md/pipeline/structured.py +142 -0
  19. any2md-1.0.1/any2md/pipeline/text.py +291 -0
  20. any2md-1.0.1/any2md/utils.py +70 -0
  21. any2md-1.0.1/any2md/validators.py +26 -0
  22. any2md-1.0.1/docs/architecture.md +578 -0
  23. any2md-1.0.1/docs/cli-reference.md +577 -0
  24. any2md-1.0.1/docs/output-format.md +774 -0
  25. any2md-1.0.1/docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md +3774 -0
  26. any2md-1.0.1/docs/superpowers/plans/2026-04-26-any2md-v1-phase2-docling-backend.md +1426 -0
  27. any2md-1.0.1/docs/superpowers/plans/2026-04-26-any2md-v1-phase3-figures-ocr-text-lane.md +1053 -0
  28. any2md-1.0.1/docs/superpowers/plans/2026-04-26-any2md-v1-phase4-polish-config-docs.md +936 -0
  29. any2md-1.0.1/docs/superpowers/specs/2026-04-26-any2md-v1-design.md +928 -0
  30. any2md-1.0.1/docs/troubleshooting.md +473 -0
  31. any2md-1.0.1/docs/upgrading-from-0.7.md +236 -0
  32. {any2md-0.6.0 → any2md-1.0.1}/pyproject.toml +11 -0
  33. any2md-1.0.1/scripts/validate-phase1.sh +32 -0
  34. any2md-1.0.1/tests/__init__.py +0 -0
  35. any2md-1.0.1/tests/cli/__init__.py +0 -0
  36. any2md-1.0.1/tests/cli/test_cli_backend.py +45 -0
  37. any2md-1.0.1/tests/cli/test_cli_existing_flags.py +52 -0
  38. any2md-1.0.1/tests/cli/test_cli_exit_codes.py +71 -0
  39. any2md-1.0.1/tests/cli/test_cli_high_fidelity.py +39 -0
  40. any2md-1.0.1/tests/cli/test_cli_meta.py +36 -0
  41. any2md-1.0.1/tests/cli/test_cli_ocr_save_images.py +34 -0
  42. any2md-1.0.1/tests/cli/test_cli_quiet_verbose.py +34 -0
  43. any2md-1.0.1/tests/cli/test_cli_strict.py +47 -0
  44. any2md-1.0.1/tests/conftest.py +25 -0
  45. any2md-1.0.1/tests/fixtures/__init__.py +0 -0
  46. any2md-1.0.1/tests/fixtures/docs/ligatures_and_softhyphens.txt +5 -0
  47. any2md-1.0.1/tests/fixtures/docs/multi_column.pdf +93 -0
  48. any2md-1.0.1/tests/fixtures/docs/table_heavy.docx +0 -0
  49. any2md-1.0.1/tests/fixtures/docs/web_page.html +35 -0
  50. any2md-1.0.1/tests/fixtures/make_fixtures.py +140 -0
  51. any2md-1.0.1/tests/fixtures/snapshots/ligatures_and_softhyphens.md +25 -0
  52. any2md-1.0.1/tests/fixtures/snapshots/multi_column.docling.md +28 -0
  53. any2md-1.0.1/tests/fixtures/snapshots/multi_column.fallback.md +31 -0
  54. any2md-1.0.1/tests/fixtures/snapshots/table_heavy.docling.md +31 -0
  55. any2md-1.0.1/tests/fixtures/snapshots/table_heavy.fallback.md +33 -0
  56. any2md-1.0.1/tests/fixtures/snapshots/web_page.md +38 -0
  57. any2md-1.0.1/tests/integration/__init__.py +0 -0
  58. any2md-1.0.1/tests/integration/test_docx_backend_override.py +57 -0
  59. any2md-1.0.1/tests/integration/test_docx_docling.py +30 -0
  60. any2md-1.0.1/tests/integration/test_docx_mammoth_fallback.py +42 -0
  61. any2md-1.0.1/tests/integration/test_html_trafilatura.py +29 -0
  62. any2md-1.0.1/tests/integration/test_pdf_backend_override.py +57 -0
  63. any2md-1.0.1/tests/integration/test_pdf_docling.py +48 -0
  64. any2md-1.0.1/tests/integration/test_pdf_pymupdf_fallback.py +37 -0
  65. any2md-1.0.1/tests/integration/test_save_images.py +24 -0
  66. any2md-1.0.1/tests/integration/test_snapshots.py +87 -0
  67. any2md-1.0.1/tests/integration/test_txt.py +32 -0
  68. any2md-1.0.1/tests/unit/__init__.py +0 -0
  69. any2md-1.0.1/tests/unit/pipeline/__init__.py +0 -0
  70. any2md-1.0.1/tests/unit/pipeline/test_cleanup_footnote_markers.py +40 -0
  71. any2md-1.0.1/tests/unit/pipeline/test_cleanup_ligatures.py +31 -0
  72. any2md-1.0.1/tests/unit/pipeline/test_cleanup_nfc.py +22 -0
  73. any2md-1.0.1/tests/unit/pipeline/test_cleanup_quotes_dashes.py +29 -0
  74. any2md-1.0.1/tests/unit/pipeline/test_cleanup_soft_hyphens.py +19 -0
  75. any2md-1.0.1/tests/unit/pipeline/test_cleanup_validate.py +32 -0
  76. any2md-1.0.1/tests/unit/pipeline/test_cleanup_whitespace.py +38 -0
  77. any2md-1.0.1/tests/unit/pipeline/test_runner.py +54 -0
  78. any2md-1.0.1/tests/unit/pipeline/test_structured_compact_tables.py +36 -0
  79. any2md-1.0.1/tests/unit/pipeline/test_structured_heading_hierarchy.py +34 -0
  80. any2md-1.0.1/tests/unit/pipeline/test_structured_lift_figures.py +40 -0
  81. any2md-1.0.1/tests/unit/pipeline/test_structured_normalize_cites.py +25 -0
  82. any2md-1.0.1/tests/unit/pipeline/test_text_dedupe_paragraphs.py +27 -0
  83. any2md-1.0.1/tests/unit/pipeline/test_text_dedupe_toc.py +72 -0
  84. any2md-1.0.1/tests/unit/pipeline/test_text_dehyphenate.py +32 -0
  85. any2md-1.0.1/tests/unit/pipeline/test_text_repair_line_wraps.py +47 -0
  86. any2md-1.0.1/tests/unit/pipeline/test_text_restore_lists_code.py +29 -0
  87. any2md-1.0.1/tests/unit/pipeline/test_text_strip_headers_footers.py +36 -0
  88. any2md-1.0.1/tests/unit/test_auto_id.py +64 -0
  89. any2md-1.0.1/tests/unit/test_config.py +52 -0
  90. any2md-1.0.1/tests/unit/test_content_hash.py +40 -0
  91. any2md-1.0.1/tests/unit/test_docling_helper.py +28 -0
  92. any2md-1.0.1/tests/unit/test_frontmatter_compose.py +138 -0
  93. any2md-1.0.1/tests/unit/test_frontmatter_helpers.py +124 -0
  94. any2md-1.0.1/tests/unit/test_meta_overrides.py +81 -0
  95. any2md-1.0.1/tests/unit/test_pdf_complexity.py +47 -0
  96. any2md-1.0.1/tests/unit/test_validators.py +34 -0
  97. any2md-0.6.0/CHANGELOG.md +0 -27
  98. any2md-0.6.0/PKG-INFO +0 -286
  99. any2md-0.6.0/README.md +0 -263
  100. any2md-0.6.0/any2md/cli.py +0 -168
  101. any2md-0.6.0/any2md/converters/__init__.py +0 -46
  102. any2md-0.6.0/any2md/converters/docx.py +0 -73
  103. any2md-0.6.0/any2md/converters/html.py +0 -248
  104. any2md-0.6.0/any2md/converters/pdf.py +0 -72
  105. any2md-0.6.0/any2md/utils.py +0 -156
  106. {any2md-0.6.0 → any2md-1.0.1}/LICENSE +0 -0
  107. {any2md-0.6.0 → any2md-1.0.1}/any2md/__main__.py +0 -0
  108. {any2md-0.6.0 → any2md-1.0.1}/requirements.txt +0 -0
@@ -34,6 +34,11 @@ output/
34
34
  Text/
35
35
  input/
36
36
 
37
+ # Local-only working dirs (not part of the package)
38
+ template/
39
+ test_docs/
40
+ .worktrees/
41
+
37
42
  # ZERG / dev tooling
38
43
  .gsd/
39
44
  .zerg/
@@ -0,0 +1,255 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [1.0.1] — 2026-04-26
8
+
9
+ Patch release. Adds an explicit backend-selection CLI flag.
10
+
11
+ ### Added
12
+ - `--backend {docling,pymupdf4llm,mammoth}` CLI flag and corresponding
13
+ `PipelineOptions.backend` field. Lets users override the automatic
14
+ backend selection. Useful when Docling extracts incorrectly on a
15
+ specific input and the user wants to fall back to pymupdf4llm
16
+ without uninstalling Docling. Mismatched format/backend combinations
17
+ (e.g., `--backend pymupdf4llm` on a DOCX) error out per file.
18
+
19
+ ### Fixes (workaround)
20
+ - Refs #13 — Docling drops list items on certain academic PDFs with
21
+ the "Parent element of the list item is not a ListGroup" warning.
22
+ No upstream fix yet; users hitting this can now `any2md --backend
23
+ pymupdf4llm doc.pdf` to use the lightweight backend instead.
24
+
25
+ ### Note
26
+ - `--backend docling` is equivalent to the existing `--high-fidelity`
27
+ / `-H` flag (both kept). Future releases may consolidate.
28
+
29
+ ## [1.0.0] — 2026-04-26
30
+
31
+ First stable release of any2md v1.0. Validated end-to-end against
32
+ real-world documents (a 164-page PDF technical standard via Docling,
33
+ a multi-page academic DOCX, and a Wikipedia article via trafilatura).
34
+ The output contract — SSRM-compatible frontmatter, deterministic
35
+ `content_hash`, NFC + LF body normalization — is now stable. Downstream
36
+ consumers can rely on this shape.
37
+
38
+ Functional contents are identical to 1.0.0rc1; only the version is bumped.
39
+
40
+ ## [1.0.0rc1] — 2026-04-26
41
+
42
+ Phase 4: configuration, polish, and the v1.0 documentation set.
43
+
44
+ ### Added
45
+ - `--profile {conservative,aggressive,maximum}` flag — tunes minimization
46
+ aggressiveness. `conservative` skips TOC dedupe and footnote-marker
47
+ stripping; `aggressive` (default) runs the full pipeline; `maximum`
48
+ additionally implies `--strip-links`.
49
+ - `--meta KEY=VAL` repeatable flag for frontmatter overrides. Dotted keys
50
+ set nested fields (e.g. `--meta generation_metadata.authored_by=human`);
51
+ comma-separated values become arrays
52
+ (e.g. `--meta authors="Alice, Bob"`).
53
+ - `--meta-file PATH` flag plus auto-discovery of `.any2md.toml` (walks up
54
+ from cwd). The `[meta]` table supplies frontmatter overrides; the
55
+ `[document_id]` table supplies `--auto-id` prefix and type code.
56
+ - `--auto-id` flag — generates an SSRM-conformant `document_id` as
57
+ `{PREFIX}-{YYYY}-{TYPE}-{SHA8}`. Defaults to `LOCAL`/`DOC`; override via
58
+ the `[document_id]` table in `.any2md.toml`.
59
+ - `--strict` flag — promotes pipeline validation warnings (heading
60
+ hierarchy auto-fixes, missing H1, `content_hash` round-trip mismatches)
61
+ to a non-zero exit.
62
+ - `--quiet` / `-q` flag — suppresses the per-file `OK:` lines. Errors and
63
+ the final summary still print.
64
+ - `--verbose` / `-v` flag — adds per-file pipeline stage timings.
65
+ - New exit code contract: `0` success, `1` usage or install error, `2` at
66
+ least one file failed entirely, `3` at least one file produced warnings
67
+ AND `--strict` was set.
68
+ - Per-file summary line now includes backend, lane, token estimate, and
69
+ warning count when present.
70
+ - New module `any2md/config.py` for `.any2md.toml` discovery and parsing
71
+ (uses stdlib `tomllib` on 3.11+, `tomli` on 3.10).
72
+ - Comprehensive documentation set:
73
+ - `README.md` rewritten with educational tone — every command preceded
74
+ by why you'd run it and followed by what you'll see.
75
+ - `docs/output-format.md` — SSRM-compatible field reference and the
76
+ `content_hash` recomputation recipe.
77
+ - `docs/cli-reference.md` — flag-by-flag reference with use cases and a
78
+ worked-example matrix.
79
+ - `docs/architecture.md` — pipeline internals, stage catalog, and
80
+ contributor guide.
81
+ - `docs/troubleshooting.md` — symptom-cause-fix triage guide.
82
+ - `docs/upgrading-from-0.7.md` — migration guide.
83
+ - `CONTRIBUTING.md` — dev setup, test commands, coding standards,
84
+ release flow, PR process.
85
+ - GitHub issue templates: generic bug report, conversion quality.
86
+
87
+ ### Changed
88
+ - `frontmatter.compose()` now accepts an `overrides: dict | None` argument
89
+ that deep-merges over derived fields. Used by `--meta`, `--meta-file`,
90
+ and `.any2md.toml`.
91
+ - `cli.main()` exit-code logic refactored to track failures and
92
+ strict-mode warnings separately, via the new
93
+ `any2md.converters.collected_warnings()` helper.
94
+ - `--profile maximum` implies `--strip-links` automatically.
95
+
96
+ ## [1.0.0a3] — 2026-04-26
97
+
98
+ Phase 3: text-lane post-processing stages and image/OCR flag wiring.
99
+
100
+ ### Added
101
+ - Text-lane stages T1–T6 now active for all non-Docling output paths
102
+ (TXT, trafilatura HTML/URL, mammoth DOCX fallback, pymupdf4llm PDF
103
+ fallback):
104
+ - **T1 `repair_line_wraps`** — joins soft-wrapped lines inside
105
+ paragraphs while preserving lists, tables, code blocks, headings.
106
+ - **T2 `dehyphenate`** — merges `co-\noperation` → `cooperation`
107
+ when the joined word appears elsewhere in the document
108
+ (same-doc corroboration; conservative).
109
+ - **T3 `dedupe_paragraphs`** — drops a paragraph if it's identical to
110
+ the immediately previous one (PDF over-extraction artifact).
111
+ - **T4 `dedupe_toc_block`** — strips leading TOC blocks when ≥70% of
112
+ their entries reappear as H2/H3 headings later. Aggressive/maximum
113
+ profiles only.
114
+ - **T5 `strip_running_headers_footers`** — removes lines that repeat
115
+ ≥3 times across page boundaries (form-feed `\f`-aware).
116
+ - **T6 `restore_lists_and_code`** — wraps ≥4-line indented blocks
117
+ in fenced code.
118
+ - New CLI flags `--ocr-figures` and `--save-images`. Both imply
119
+ `--high-fidelity`. With `--save-images`, the PDF Docling path writes
120
+ extracted images to `<output_dir>/images/<source_stem>/imgN.png`.
121
+
122
+ ### Changed
123
+ - `repair_line_wraps` runs before `dehyphenate` so hyphens that span
124
+ joined lines are merged correctly.
125
+ - Snapshot fixtures regenerated to reflect text-lane stage output.
126
+
127
+ ## [1.0.0a2] — 2026-04-26
128
+
129
+ Phase 2: Docling backend integration. PDFs and DOCX files can now be
130
+ extracted via Docling for substantially higher fidelity on multi-column
131
+ layouts and complex tables. Docling is an optional install — without it,
132
+ any2md transparently falls back to pymupdf4llm (PDF) and mammoth (DOCX).
133
+
134
+ ### Added
135
+ - New optional dependency: `pip install "any2md[high-fidelity]"` installs
136
+ Docling, which becomes the primary extraction backend for PDF and DOCX.
137
+ - `any2md/_docling.py` — detection helper plus a rate-limited install hint
138
+ that surfaces on the stderr when an artifact-prone PDF is being converted
139
+ without Docling installed.
140
+ - New CLI flag: `--high-fidelity` / `-H`. Forces Docling. Exits with code 1
141
+ and prints the install hint when Docling is not present.
142
+ - `pdf_looks_complex(pdf_path)` heuristic — fast (< 50 ms) check used to
143
+ decide whether to print the install hint when Docling is missing.
144
+ - New `PipelineOptions.high_fidelity` field (forwarded by the CLI).
145
+ - Structured-lane post-processing stages now active for Docling-emitted
146
+ markdown:
147
+ - **S1 `lift_figure_captions`** — converts image markdown and HTML
148
+ `<figure>` blocks to italic `*Figure: caption*` lines. Drops image
149
+ references (preserved with `--save-images`, planned for Phase 3).
150
+ - **S2 `compact_tables`** — strips per-cell padding spaces in GFM tables;
151
+ preserves alignment row intact.
152
+ - **S3 `normalize_citations`** — coalesces `[1] [2] [3]` → `[1][2][3]`.
153
+ - **S4 `enforce_heading_hierarchy`** — guarantees a single H1 (promotes
154
+ first heading if needed; demotes subsequent H1s); flattens skipped
155
+ levels (H2 → H4 becomes H2 → H3).
156
+ - Per-backend snapshot tests: PDF and DOCX now have `.docling.md` and
157
+ `.fallback.md` golden outputs.
158
+
159
+ ### Changed
160
+ - PDF and DOCX converters select backend at runtime: Docling if importable,
161
+ pymupdf4llm/mammoth otherwise. Fallback also fires if Docling raises
162
+ during extraction (with stderr warning).
163
+ - `extracted_via` frontmatter field reports `docling` for the new path.
164
+ - `lane` is `"structured"` for Docling output, `"text"` for fallback —
165
+ the structured lane runs S1–S4 before shared cleanup; the text lane
166
+ runs only shared cleanup in Phase 2 (T1–T6 land in Phase 3).
167
+
168
+ ### Carried forward
169
+ - All v0.7 + v1.0.0a1 CLI flags.
170
+ - HTML/URL → trafilatura (Docling not used for web — purpose-built
171
+ boilerplate stripping is more important than structured layout).
172
+ - TXT → heuristic `structurize()`.
173
+
174
+ ## [1.0.0a1] — 2026-04-26
175
+
176
+ First prerelease of any2md v1.0. Phase 1 of 5: foundation only — no Docling
177
+ backend yet, no new CLI flags. Output frontmatter has been rewritten to be
178
+ SSRM-compatible (Structured Security Reasoning Markdown) — this is a
179
+ breaking change for downstream consumers parsing v0.7 output. See
180
+ `docs/superpowers/specs/2026-04-26-any2md-v1-design.md` for the full
181
+ v1.0 design and `docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md`
182
+ for this phase's task plan.
183
+
184
+ ### Added
185
+ - New `any2md/frontmatter.py` module — SSRM-compatible YAML emitter with the
186
+ `SourceMeta` dataclass, deterministic `compute_content_hash`,
187
+ `estimate_tokens`, `recommend_chunk_level`, `extract_abstract`, and
188
+ `derive_title` helpers, plus the central `compose()` function (the only
189
+ producer of the YAML block — converters never touch YAML directly).
190
+ - New `any2md/pipeline/` package with the two-lane post-processing runner
191
+ (`structured` and `text` lanes both empty in Phase 1; filled in Phase 2/3)
192
+ and seven shared cleanup stages C1–C7 in `pipeline/cleanup.py`:
193
+ NFC normalization, soft-hyphen strip, ligature normalization (whitelist
194
+ driven, NOT blanket NFKC), smart-quote/dash normalization, whitespace
195
+ collapse, footnote-marker strip (aggressive/maximum profiles only), and
196
+ read-only validation that emits non-fatal warnings via a contextvar.
197
+ - New `any2md/validators.py` — heading-hierarchy and `content_hash` round-trip
198
+ checks. Used by C7 and exposed for programmatic consumers.
199
+ - pytest test suite: 86 tests covering pipeline stages, frontmatter helpers,
200
+ content_hash determinism (including known empty-string SHA-256 vector),
201
+ validators, all four converters end-to-end, CLI smoke tests, and
202
+ snapshot tests for golden output.
203
+ - Synthetic test fixtures (HTML, TXT, multi-column PDF, table-heavy DOCX)
204
+ under `tests/fixtures/docs/` — all owned, deterministic, and small.
205
+ Generator script `tests/fixtures/make_fixtures.py` regenerates the binary
206
+ fixtures (PDF via reportlab, DOCX via direct zip+XML — no python-docx dep).
207
+ - `[dev]` extras in `pyproject.toml`: `pytest`, `pytest-snapshot`,
208
+ `reportlab`, `ruff`.
209
+
210
+ ### Changed
211
+ - **BREAKING:** Output frontmatter shape is now SSRM-compatible. New required
212
+ fields: `document_id` (empty string by default — opt-in via future
213
+ `--auto-id`), `version`, `date`, `status: "draft"`, `document_type` (empty
214
+ for non-security documents), `content_domain` (empty array),
215
+ `authors`, `organization`, `generation_metadata.authored_by: "unknown"`,
216
+ and `content_hash` (SHA-256 of NFC + LF body). Optional fields auto-filled
217
+ when derivable: `keywords`, `token_estimate`, `recommended_chunk_level`,
218
+ `abstract_for_rag` (only for documents ≥ 500 estimated tokens).
219
+ - v0.7 fields `source_file`, `source_url`, `pages`, `word_count`, and `type`
220
+ are retained as any2md extension fields for traceability. A new
221
+ `extracted_via` extension field records which library produced the
222
+ markdown (`pymupdf4llm` / `mammoth+markdownify` / `trafilatura` /
223
+ `trafilatura+bs4_fallback` / `heuristic`).
224
+ - Body is NFC-normalized with LF line endings before write — outputs are
225
+ byte-deterministic for a given input + flag set.
226
+ - All four converters now route through the shared post-processing pipeline
227
+ via `pipeline.run(text, lane, options)` and emit through
228
+ `frontmatter.compose(body, meta, options)`. Converters return
229
+ `(markdown, SourceMeta)` shape internally — they no longer touch YAML.
230
+ - DOCX metadata extraction: now reads `docProps/core.xml` and
231
+ `docProps/app.xml` directly via `zipfile + xml.etree.ElementTree`. No
232
+ python-docx dependency added.
233
+ - HTML converter: uses `trafilatura.bare_extraction(...)` for metadata
234
+ (title/author/sitename/date/categories). For URL inputs, a single
235
+ best-effort HEAD request captures HTTP `Last-Modified`.
236
+ - `.gitignore` now excludes `template/`, `test_docs/`, and `.worktrees/`.
237
+
238
+ ### Carried forward unchanged
239
+ - All v0.7 CLI flags (`--input-dir`, `--output-dir`, `--force`,
240
+ `--strip-links`, `--recursive`, `--max-file-size`, positional file/URL/dir
241
+ arguments). The `--strip-links` flag is now wired through `PipelineOptions`.
242
+ - SSRF protection on URL fetching (private/reserved/loopback IP rejection).
243
+ - File size limits (default 100 MB).
244
+ - Filename sanitization rules.
245
+ - Skip-if-exists unless `--force`.
246
+ - pymupdf4llm, mammoth, markdownify, trafilatura, BeautifulSoup, lxml
247
+ backends.
248
+ - Python 3.10+ floor.
249
+
250
+ ### Out of scope for Phase 1
251
+ Phase 2 (Docling backend, `--high-fidelity`, structured-lane stages S1–S4),
252
+ Phase 3 (figure/OCR handling, text-lane stages T1–T6, full metadata
253
+ extraction), Phase 4 (`--meta`, `.any2md.toml`, `--auto-id`, `--strict`,
254
+ `--quiet`, `--verbose`, full documentation rewrite), Phase 5 (release
255
+ validation, 1.0.0).
@@ -0,0 +1,148 @@
1
+ # Contributing to any2md
2
+
3
+ This guide covers everything you need to make a change to any2md and get it
4
+ merged: development setup, the test commands you'll run, where the code
5
+ lives, the coding standards, and the release flow.
6
+
7
+ For a user-facing orientation, see the [README](README.md). For pipeline
8
+ internals and the contracts that converters and pipeline stages have to
9
+ honor, see [docs/architecture.md](docs/architecture.md). For the output
10
+ shape any change must continue to produce, see
11
+ [docs/output-format.md](docs/output-format.md).
12
+
13
+ ## Development setup
14
+
15
+ Clone the repo and install the editable package with both extras. `dev`
16
+ brings in `pytest`, `pytest-snapshot`, `reportlab` (used by the fixture
17
+ generator), and `ruff`. `high-fidelity` brings in Docling and roughly 2 GB
18
+ of ML models on first use:
19
+
20
+ ```bash
21
+ git clone https://github.com/rocklambros/any2md.git
22
+ cd any2md
23
+ pip install -e ".[dev,high-fidelity]"
24
+ pytest
25
+ ```
26
+
27
+ You can skip `high-fidelity` if you're working on a non-Docling code path —
28
+ the test suite gates Docling-dependent tests with
29
+ `@pytest.mark.skipif(not has_docling(), …)` so they're transparently skipped
30
+ without it. Coverage of fallback paths is unaffected.
31
+
32
+ Python 3.10 or later is required.
33
+
34
+ ## Running tests
35
+
36
+ The default invocation runs the whole suite (currently 172 tests):
37
+
38
+ ```bash
39
+ pytest -v
40
+ ```
41
+
42
+ Tests are organized by scope:
43
+
44
+ - `tests/unit/` — pure-function tests for pipeline stages, frontmatter
45
+ helpers, validators, and config parsing.
46
+ - `tests/cli/` — `subprocess`-driven tests that exercise the CLI end to end.
47
+ - `tests/integration/` — full converter runs over real fixtures, including
48
+ Docling-vs-fallback parity checks.
49
+
50
+ The integration suite includes snapshot tests that compare converter output
51
+ against committed golden files under `tests/fixtures/snapshots/`. When you
52
+ make an intentional output change (new pipeline stage, fixed bug that
53
+ shifted a hash, etc.), regenerate the snapshots:
54
+
55
+ ```bash
56
+ UPDATE_SNAPSHOTS=1 pytest tests/integration/test_snapshots.py
57
+ ```
58
+
59
+ Then inspect the diff (`git diff tests/fixtures/snapshots/`) before
60
+ committing — the snapshots are review material, not just bytes.
61
+
62
+ A few tests are network-dependent (the URL converter integration suite)
63
+ and opt-in. Run them explicitly when touching `converters/html.py` or the
64
+ SSRF protection:
65
+
66
+ ```bash
67
+ pytest tests/integration/test_url_wikipedia.py -v
68
+ ```
69
+
70
+ ## Adding a new converter
71
+
72
+ The full step-by-step is in
73
+ [docs/architecture.md#adding-a-new-converter](docs/architecture.md#adding-a-new-converter).
74
+ The short version: write a `convert_<format>` function in
75
+ `any2md/converters/<format>.py` that returns `(markdown, SourceMeta)` and
76
+ declares its lane (`"structured"` or `"text"`). Register it in
77
+ `any2md/cli.py`'s dispatch logic. The frontmatter emitter and the cleanup
78
+ pipeline are not the converter's job — keep them centralized.
79
+
80
+ ## Adding a new pipeline stage
81
+
82
+ The full step-by-step is in
83
+ [docs/architecture.md#adding-a-new-pipeline-stage](docs/architecture.md#adding-a-new-pipeline-stage).
84
+ The short version: pick the lane (`structured`, `text`, or shared
85
+ `cleanup`), write a function with the signature
86
+ `(text: str, options: PipelineOptions) -> str`, register it in the lane's
87
+ `STAGES` list in dependency order, and add a unit test. One test file per
88
+ stage; follow the red-then-green TDD rhythm and commit when green.
89
+
90
+ ## Coding standards
91
+
92
+ - **Lazy imports for optional dependencies.** Anything pulled in by an
93
+ extra (Docling, reportlab) is imported inside the function that uses it,
94
+ not at module top, so the lightweight install path doesn't pay an import
95
+ cost or crash on missing packages.
96
+ - **No class hierarchies for simple operations.** Pipeline stages are
97
+ module-level functions, not subclasses of an abstract `Stage` base.
98
+ Frontmatter helpers are functions, not methods on a `Frontmatter` class.
99
+ This is a deliberate choice — the operations are stateless and the
100
+ resulting code is easier to read.
101
+ - **`ruff check` and `ruff format` clean.** The repo runs ruff in CI; PRs
102
+ with lint errors won't merge.
103
+ - **Docstrings.** One-line summary, blank line, optional sections (Args,
104
+ Returns, Raises, Examples). Match the style already in
105
+ `any2md/frontmatter.py` and `any2md/pipeline/cleanup.py`.
106
+ - **Tests.** One file per stage. Red-then-green TDD: write the failing
107
+ test first, then the minimal implementation. Commit when green. Snapshot
108
+ tests cover whole-converter behavior; unit tests cover individual stages.
109
+
110
+ ## Release flow
111
+
112
+ Releases are driven by GitHub Releases. The
113
+ [`publish.yml`](.github/workflows/publish.yml) workflow inspects the
114
+ release's `prerelease` flag: when `prerelease == true`, the artifacts go
115
+ to TestPyPI; when `false`, they go to PyPI.
116
+
117
+ The flow:
118
+
119
+ 1. Bump `__version__` in `any2md/__init__.py` (e.g., `1.0.0rc2` for a
120
+ prerelease, `1.0.0` for a stable).
121
+ 2. Update `CHANGELOG.md` with a new section dated to the release.
122
+ 3. Open a PR with the version bump and the changelog entry. Land it.
123
+ 4. Tag the release commit on `main` (e.g., `git tag -a v1.0.0rc2 -m "any2md 1.0.0rc2"`)
124
+ and push the tag.
125
+ 5. Create a GitHub Release pointing at the tag. **Mark it as a prerelease**
126
+ if the version contains `a`, `b`, or `rc` — that's what routes the
127
+ workflow to TestPyPI rather than PyPI.
128
+ 6. Watch the workflow. The version-tag check will fail loudly if the tag
129
+ and `__version__` disagree.
130
+
131
+ Don't tag and release directly without the PR — CI gates exist for a
132
+ reason.
133
+
134
+ ## Pull request process
135
+
136
+ - Branch off `main` (or off a phase branch like `v1.0` if one is open for
137
+ in-progress work — check the recent activity before branching).
138
+ - One logical change per PR. A PR that adds a converter and rewrites the
139
+ HTML lane is two PRs.
140
+ - Tests green and `ruff check` clean before requesting review.
141
+ - Descriptive PR title — same shape as the commit messages in
142
+ `git log --oneline` (`feat(cli): ...`, `fix(pipeline): ...`,
143
+ `docs: ...`). The title becomes the squash-merge commit message.
144
+ - Conversion-quality bug reports are the highest-value issues. If you're
145
+ filing one, please use the `Conversion quality` issue template — it asks
146
+ for the source format, file size, Docling version, full command, and a
147
+ 5-line snippet of the bad output, all of which are typically required to
148
+ reproduce the artifact.