any2md 0.5.0__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {any2md-0.5.0 → any2md-1.0.0}/.gitignore +5 -0
  2. any2md-1.0.0/CHANGELOG.md +233 -0
  3. any2md-1.0.0/CONTRIBUTING.md +148 -0
  4. any2md-1.0.0/PKG-INFO +529 -0
  5. any2md-1.0.0/README.md +499 -0
  6. {any2md-0.5.0 → any2md-1.0.0}/any2md/__init__.py +1 -1
  7. {any2md-0.5.0 → any2md-1.0.0}/any2md/__main__.py +1 -0
  8. any2md-1.0.0/any2md/_docling.py +37 -0
  9. any2md-1.0.0/any2md/cli.py +403 -0
  10. any2md-1.0.0/any2md/config.py +81 -0
  11. any2md-1.0.0/any2md/converters/__init__.py +86 -0
  12. any2md-1.0.0/any2md/converters/docx.py +168 -0
  13. any2md-1.0.0/any2md/converters/html.py +261 -0
  14. any2md-1.0.0/any2md/converters/pdf.py +253 -0
  15. {any2md-0.5.0 → any2md-1.0.0}/any2md/converters/txt.py +68 -74
  16. any2md-1.0.0/any2md/frontmatter.py +304 -0
  17. any2md-1.0.0/any2md/pipeline/__init__.py +73 -0
  18. any2md-1.0.0/any2md/pipeline/cleanup.py +129 -0
  19. any2md-1.0.0/any2md/pipeline/structured.py +142 -0
  20. any2md-1.0.0/any2md/pipeline/text.py +291 -0
  21. any2md-1.0.0/any2md/utils.py +70 -0
  22. any2md-1.0.0/any2md/validators.py +26 -0
  23. any2md-1.0.0/docs/architecture.md +578 -0
  24. any2md-1.0.0/docs/cli-reference.md +541 -0
  25. any2md-1.0.0/docs/output-format.md +774 -0
  26. any2md-1.0.0/docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md +3766 -0
  27. any2md-1.0.0/docs/superpowers/specs/2026-04-26-any2md-v1-design.md +928 -0
  28. any2md-1.0.0/docs/troubleshooting.md +473 -0
  29. any2md-1.0.0/docs/upgrading-from-0.7.md +236 -0
  30. {any2md-0.5.0 → any2md-1.0.0}/pyproject.toml +19 -7
  31. any2md-1.0.0/requirements.txt +7 -0
  32. any2md-1.0.0/scripts/validate-phase1.sh +32 -0
  33. any2md-1.0.0/tests/__init__.py +0 -0
  34. any2md-1.0.0/tests/cli/__init__.py +0 -0
  35. any2md-1.0.0/tests/cli/test_cli_existing_flags.py +52 -0
  36. any2md-1.0.0/tests/cli/test_cli_exit_codes.py +71 -0
  37. any2md-1.0.0/tests/cli/test_cli_high_fidelity.py +39 -0
  38. any2md-1.0.0/tests/cli/test_cli_meta.py +36 -0
  39. any2md-1.0.0/tests/cli/test_cli_ocr_save_images.py +34 -0
  40. any2md-1.0.0/tests/cli/test_cli_quiet_verbose.py +34 -0
  41. any2md-1.0.0/tests/cli/test_cli_strict.py +47 -0
  42. any2md-1.0.0/tests/conftest.py +25 -0
  43. any2md-1.0.0/tests/fixtures/__init__.py +0 -0
  44. any2md-1.0.0/tests/fixtures/docs/ligatures_and_softhyphens.txt +5 -0
  45. any2md-1.0.0/tests/fixtures/docs/multi_column.pdf +93 -0
  46. any2md-1.0.0/tests/fixtures/docs/table_heavy.docx +0 -0
  47. any2md-1.0.0/tests/fixtures/docs/web_page.html +35 -0
  48. any2md-1.0.0/tests/fixtures/make_fixtures.py +140 -0
  49. any2md-1.0.0/tests/fixtures/snapshots/ligatures_and_softhyphens.md +25 -0
  50. any2md-1.0.0/tests/fixtures/snapshots/multi_column.docling.md +28 -0
  51. any2md-1.0.0/tests/fixtures/snapshots/multi_column.fallback.md +31 -0
  52. any2md-1.0.0/tests/fixtures/snapshots/table_heavy.docling.md +31 -0
  53. any2md-1.0.0/tests/fixtures/snapshots/table_heavy.fallback.md +33 -0
  54. any2md-1.0.0/tests/fixtures/snapshots/web_page.md +38 -0
  55. any2md-1.0.0/tests/integration/__init__.py +0 -0
  56. any2md-1.0.0/tests/integration/test_docx_docling.py +30 -0
  57. any2md-1.0.0/tests/integration/test_docx_mammoth_fallback.py +42 -0
  58. any2md-1.0.0/tests/integration/test_html_trafilatura.py +29 -0
  59. any2md-1.0.0/tests/integration/test_pdf_docling.py +48 -0
  60. any2md-1.0.0/tests/integration/test_pdf_pymupdf_fallback.py +37 -0
  61. any2md-1.0.0/tests/integration/test_save_images.py +24 -0
  62. any2md-1.0.0/tests/integration/test_snapshots.py +87 -0
  63. any2md-1.0.0/tests/integration/test_txt.py +32 -0
  64. any2md-1.0.0/tests/unit/__init__.py +0 -0
  65. any2md-1.0.0/tests/unit/pipeline/__init__.py +0 -0
  66. any2md-1.0.0/tests/unit/pipeline/test_cleanup_footnote_markers.py +40 -0
  67. any2md-1.0.0/tests/unit/pipeline/test_cleanup_ligatures.py +31 -0
  68. any2md-1.0.0/tests/unit/pipeline/test_cleanup_nfc.py +22 -0
  69. any2md-1.0.0/tests/unit/pipeline/test_cleanup_quotes_dashes.py +29 -0
  70. any2md-1.0.0/tests/unit/pipeline/test_cleanup_soft_hyphens.py +19 -0
  71. any2md-1.0.0/tests/unit/pipeline/test_cleanup_validate.py +32 -0
  72. any2md-1.0.0/tests/unit/pipeline/test_cleanup_whitespace.py +38 -0
  73. any2md-1.0.0/tests/unit/pipeline/test_runner.py +44 -0
  74. any2md-1.0.0/tests/unit/pipeline/test_structured_compact_tables.py +36 -0
  75. any2md-1.0.0/tests/unit/pipeline/test_structured_heading_hierarchy.py +34 -0
  76. any2md-1.0.0/tests/unit/pipeline/test_structured_lift_figures.py +40 -0
  77. any2md-1.0.0/tests/unit/pipeline/test_structured_normalize_cites.py +25 -0
  78. any2md-1.0.0/tests/unit/pipeline/test_text_dedupe_paragraphs.py +27 -0
  79. any2md-1.0.0/tests/unit/pipeline/test_text_dedupe_toc.py +72 -0
  80. any2md-1.0.0/tests/unit/pipeline/test_text_dehyphenate.py +32 -0
  81. any2md-1.0.0/tests/unit/pipeline/test_text_repair_line_wraps.py +47 -0
  82. any2md-1.0.0/tests/unit/pipeline/test_text_restore_lists_code.py +29 -0
  83. any2md-1.0.0/tests/unit/pipeline/test_text_strip_headers_footers.py +36 -0
  84. any2md-1.0.0/tests/unit/test_auto_id.py +64 -0
  85. any2md-1.0.0/tests/unit/test_config.py +52 -0
  86. any2md-1.0.0/tests/unit/test_content_hash.py +40 -0
  87. any2md-1.0.0/tests/unit/test_docling_helper.py +28 -0
  88. any2md-1.0.0/tests/unit/test_frontmatter_compose.py +138 -0
  89. any2md-1.0.0/tests/unit/test_frontmatter_helpers.py +124 -0
  90. any2md-1.0.0/tests/unit/test_meta_overrides.py +81 -0
  91. any2md-1.0.0/tests/unit/test_pdf_complexity.py +47 -0
  92. any2md-1.0.0/tests/unit/test_validators.py +34 -0
  93. any2md-0.5.0/CHANGELOG.md +0 -27
  94. any2md-0.5.0/PKG-INFO +0 -272
  95. any2md-0.5.0/README.md +0 -250
  96. any2md-0.5.0/any2md/cli.py +0 -145
  97. any2md-0.5.0/any2md/converters/__init__.py +0 -34
  98. any2md-0.5.0/any2md/converters/docx.py +0 -72
  99. any2md-0.5.0/any2md/converters/html.py +0 -162
  100. any2md-0.5.0/any2md/converters/pdf.py +0 -73
  101. any2md-0.5.0/any2md/utils.py +0 -86
  102. any2md-0.5.0/requirements.txt +0 -6
  103. {any2md-0.5.0 → any2md-1.0.0}/LICENSE +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,233 @@
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.0] — 2026-04-26
8
+
9
+ First stable release of any2md v1.0. Validated end-to-end against
10
+ real-world documents (a 164-page PDF technical standard via Docling,
11
+ a multi-page academic DOCX, and a Wikipedia article via trafilatura).
12
+ The output contract — SSRM-compatible frontmatter, deterministic
13
+ `content_hash`, NFC + LF body normalization — is now stable. Downstream
14
+ consumers can rely on this shape.
15
+
16
+ Functional contents are identical to 1.0.0rc1; only the version is bumped.
17
+
18
+ ## [1.0.0rc1] — 2026-04-26
19
+
20
+ Phase 4: configuration, polish, and the v1.0 documentation set.
21
+
22
+ ### Added
23
+ - `--profile {conservative,aggressive,maximum}` flag — tunes minimization
24
+ aggressiveness. `conservative` skips TOC dedupe and footnote-marker
25
+ stripping; `aggressive` (default) runs the full pipeline; `maximum`
26
+ additionally implies `--strip-links`.
27
+ - `--meta KEY=VAL` repeatable flag for frontmatter overrides. Dotted keys
28
+ set nested fields (e.g. `--meta generation_metadata.authored_by=human`);
29
+ comma-separated values become arrays
30
+ (e.g. `--meta authors="Alice, Bob"`).
31
+ - `--meta-file PATH` flag plus auto-discovery of `.any2md.toml` (walks up
32
+ from cwd). The `[meta]` table supplies frontmatter overrides; the
33
+ `[document_id]` table supplies `--auto-id` prefix and type code.
34
+ - `--auto-id` flag — generates an SSRM-conformant `document_id` as
35
+ `{PREFIX}-{YYYY}-{TYPE}-{SHA8}`. Defaults to `LOCAL`/`DOC`; override via
36
+ the `[document_id]` table in `.any2md.toml`.
37
+ - `--strict` flag — promotes pipeline validation warnings (heading
38
+ hierarchy auto-fixes, missing H1, `content_hash` round-trip mismatches)
39
+ to a non-zero exit.
40
+ - `--quiet` / `-q` flag — suppresses the per-file `OK:` lines. Errors and
41
+ the final summary still print.
42
+ - `--verbose` / `-v` flag — adds per-file pipeline stage timings.
43
+ - New exit code contract: `0` success, `1` usage or install error, `2` at
44
+ least one file failed entirely, `3` at least one file produced warnings
45
+ AND `--strict` was set.
46
+ - Per-file summary line now includes backend, lane, token estimate, and
47
+ warning count when present.
48
+ - New module `any2md/config.py` for `.any2md.toml` discovery and parsing
49
+ (uses stdlib `tomllib` on 3.11+, `tomli` on 3.10).
50
+ - Comprehensive documentation set:
51
+ - `README.md` rewritten with educational tone — every command preceded
52
+ by why you'd run it and followed by what you'll see.
53
+ - `docs/output-format.md` — SSRM-compatible field reference and the
54
+ `content_hash` recomputation recipe.
55
+ - `docs/cli-reference.md` — flag-by-flag reference with use cases and a
56
+ worked-example matrix.
57
+ - `docs/architecture.md` — pipeline internals, stage catalog, and
58
+ contributor guide.
59
+ - `docs/troubleshooting.md` — symptom-cause-fix triage guide.
60
+ - `docs/upgrading-from-0.7.md` — migration guide.
61
+ - `CONTRIBUTING.md` — dev setup, test commands, coding standards,
62
+ release flow, PR process.
63
+ - GitHub issue templates: generic bug report, conversion quality.
64
+
65
+ ### Changed
66
+ - `frontmatter.compose()` now accepts an `overrides: dict | None` argument
67
+ that deep-merges over derived fields. Used by `--meta`, `--meta-file`,
68
+ and `.any2md.toml`.
69
+ - `cli.main()` exit-code logic refactored to track failures and
70
+ strict-mode warnings separately, via the new
71
+ `any2md.converters.collected_warnings()` helper.
72
+ - `--profile maximum` implies `--strip-links` automatically.
73
+
74
+ ## [1.0.0a3] — 2026-04-26
75
+
76
+ Phase 3: text-lane post-processing stages and image/OCR flag wiring.
77
+
78
+ ### Added
79
+ - Text-lane stages T1–T6 now active for all non-Docling output paths
80
+ (TXT, trafilatura HTML/URL, mammoth DOCX fallback, pymupdf4llm PDF
81
+ fallback):
82
+ - **T1 `repair_line_wraps`** — joins soft-wrapped lines inside
83
+ paragraphs while preserving lists, tables, code blocks, headings.
84
+ - **T2 `dehyphenate`** — merges `co-\noperation` → `cooperation`
85
+ when the joined word appears elsewhere in the document
86
+ (same-doc corroboration; conservative).
87
+ - **T3 `dedupe_paragraphs`** — drops a paragraph if it's identical to
88
+ the immediately previous one (PDF over-extraction artifact).
89
+ - **T4 `dedupe_toc_block`** — strips leading TOC blocks when ≥70% of
90
+ their entries reappear as H2/H3 headings later. Aggressive/maximum
91
+ profiles only.
92
+ - **T5 `strip_running_headers_footers`** — removes lines that repeat
93
+ ≥3 times across page boundaries (form-feed `\f`-aware).
94
+ - **T6 `restore_lists_and_code`** — wraps ≥4-line indented blocks
95
+ in fenced code.
96
+ - New CLI flags `--ocr-figures` and `--save-images`. Both imply
97
+ `--high-fidelity`. With `--save-images`, the PDF Docling path writes
98
+ extracted images to `<output_dir>/images/<source_stem>/imgN.png`.
99
+
100
+ ### Changed
101
+ - `repair_line_wraps` runs before `dehyphenate` so hyphens that span
102
+ joined lines are merged correctly.
103
+ - Snapshot fixtures regenerated to reflect text-lane stage output.
104
+
105
+ ## [1.0.0a2] — 2026-04-26
106
+
107
+ Phase 2: Docling backend integration. PDFs and DOCX files can now be
108
+ extracted via Docling for substantially higher fidelity on multi-column
109
+ layouts and complex tables. Docling is an optional install — without it,
110
+ any2md transparently falls back to pymupdf4llm (PDF) and mammoth (DOCX).
111
+
112
+ ### Added
113
+ - New optional dependency: `pip install "any2md[high-fidelity]"` installs
114
+ Docling, which becomes the primary extraction backend for PDF and DOCX.
115
+ - `any2md/_docling.py` — detection helper plus a rate-limited install hint
116
+ that surfaces on the stderr when an artifact-prone PDF is being converted
117
+ without Docling installed.
118
+ - New CLI flag: `--high-fidelity` / `-H`. Forces Docling. Exits with code 1
119
+ and prints the install hint when Docling is not present.
120
+ - `pdf_looks_complex(pdf_path)` heuristic — fast (< 50 ms) check used to
121
+ decide whether to print the install hint when Docling is missing.
122
+ - New `PipelineOptions.high_fidelity` field (forwarded by the CLI).
123
+ - Structured-lane post-processing stages now active for Docling-emitted
124
+ markdown:
125
+ - **S1 `lift_figure_captions`** — converts image markdown and HTML
126
+ `<figure>` blocks to italic `*Figure: caption*` lines. Drops image
127
+ references (preserved with `--save-images`, planned for Phase 3).
128
+ - **S2 `compact_tables`** — strips per-cell padding spaces in GFM tables;
129
+ preserves alignment row intact.
130
+ - **S3 `normalize_citations`** — coalesces `[1] [2] [3]` → `[1][2][3]`.
131
+ - **S4 `enforce_heading_hierarchy`** — guarantees a single H1 (promotes
132
+ first heading if needed; demotes subsequent H1s); flattens skipped
133
+ levels (H2 → H4 becomes H2 → H3).
134
+ - Per-backend snapshot tests: PDF and DOCX now have `.docling.md` and
135
+ `.fallback.md` golden outputs.
136
+
137
+ ### Changed
138
+ - PDF and DOCX converters select backend at runtime: Docling if importable,
139
+ pymupdf4llm/mammoth otherwise. Fallback also fires if Docling raises
140
+ during extraction (with stderr warning).
141
+ - `extracted_via` frontmatter field reports `docling` for the new path.
142
+ - `lane` is `"structured"` for Docling output, `"text"` for fallback —
143
+ the structured lane runs S1–S4 before shared cleanup; the text lane
144
+ runs only shared cleanup in Phase 2 (T1–T6 land in Phase 3).
145
+
146
+ ### Carried forward
147
+ - All v0.7 + v1.0.0a1 CLI flags.
148
+ - HTML/URL → trafilatura (Docling not used for web — purpose-built
149
+ boilerplate stripping is more important than structured layout).
150
+ - TXT → heuristic `structurize()`.
151
+
152
+ ## [1.0.0a1] — 2026-04-26
153
+
154
+ First prerelease of any2md v1.0. Phase 1 of 5: foundation only — no Docling
155
+ backend yet, no new CLI flags. Output frontmatter has been rewritten to be
156
+ SSRM-compatible (Structured Security Reasoning Markdown) — this is a
157
+ breaking change for downstream consumers parsing v0.7 output. See
158
+ `docs/superpowers/specs/2026-04-26-any2md-v1-design.md` for the full
159
+ v1.0 design and `docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md`
160
+ for this phase's task plan.
161
+
162
+ ### Added
163
+ - New `any2md/frontmatter.py` module — SSRM-compatible YAML emitter with the
164
+ `SourceMeta` dataclass, deterministic `compute_content_hash`,
165
+ `estimate_tokens`, `recommend_chunk_level`, `extract_abstract`, and
166
+ `derive_title` helpers, plus the central `compose()` function (the only
167
+ producer of the YAML block — converters never touch YAML directly).
168
+ - New `any2md/pipeline/` package with the two-lane post-processing runner
169
+ (`structured` and `text` lanes both empty in Phase 1; filled in Phase 2/3)
170
+ and seven shared cleanup stages C1–C7 in `pipeline/cleanup.py`:
171
+ NFC normalization, soft-hyphen strip, ligature normalization (whitelist
172
+ driven, NOT blanket NFKC), smart-quote/dash normalization, whitespace
173
+ collapse, footnote-marker strip (aggressive/maximum profiles only), and
174
+ read-only validation that emits non-fatal warnings via a contextvar.
175
+ - New `any2md/validators.py` — heading-hierarchy and `content_hash` round-trip
176
+ checks. Used by C7 and exposed for programmatic consumers.
177
+ - pytest test suite: 86 tests covering pipeline stages, frontmatter helpers,
178
+ content_hash determinism (including known empty-string SHA-256 vector),
179
+ validators, all four converters end-to-end, CLI smoke tests, and
180
+ snapshot tests for golden output.
181
+ - Synthetic test fixtures (HTML, TXT, multi-column PDF, table-heavy DOCX)
182
+ under `tests/fixtures/docs/` — all owned, deterministic, and small.
183
+ Generator script `tests/fixtures/make_fixtures.py` regenerates the binary
184
+ fixtures (PDF via reportlab, DOCX via direct zip+XML — no python-docx dep).
185
+ - `[dev]` extras in `pyproject.toml`: `pytest`, `pytest-snapshot`,
186
+ `reportlab`, `ruff`.
187
+
188
+ ### Changed
189
+ - **BREAKING:** Output frontmatter shape is now SSRM-compatible. New required
190
+ fields: `document_id` (empty string by default — opt-in via future
191
+ `--auto-id`), `version`, `date`, `status: "draft"`, `document_type` (empty
192
+ for non-security documents), `content_domain` (empty array),
193
+ `authors`, `organization`, `generation_metadata.authored_by: "unknown"`,
194
+ and `content_hash` (SHA-256 of NFC + LF body). Optional fields auto-filled
195
+ when derivable: `keywords`, `token_estimate`, `recommended_chunk_level`,
196
+ `abstract_for_rag` (only for documents ≥ 500 estimated tokens).
197
+ - v0.7 fields `source_file`, `source_url`, `pages`, `word_count`, and `type`
198
+ are retained as any2md extension fields for traceability. A new
199
+ `extracted_via` extension field records which library produced the
200
+ markdown (`pymupdf4llm` / `mammoth+markdownify` / `trafilatura` /
201
+ `trafilatura+bs4_fallback` / `heuristic`).
202
+ - Body is NFC-normalized with LF line endings before write — outputs are
203
+ byte-deterministic for a given input + flag set.
204
+ - All four converters now route through the shared post-processing pipeline
205
+ via `pipeline.run(text, lane, options)` and emit through
206
+ `frontmatter.compose(body, meta, options)`. Converters return
207
+ `(markdown, SourceMeta)` shape internally — they no longer touch YAML.
208
+ - DOCX metadata extraction: now reads `docProps/core.xml` and
209
+ `docProps/app.xml` directly via `zipfile + xml.etree.ElementTree`. No
210
+ python-docx dependency added.
211
+ - HTML converter: uses `trafilatura.bare_extraction(...)` for metadata
212
+ (title/author/sitename/date/categories). For URL inputs, a single
213
+ best-effort HEAD request captures HTTP `Last-Modified`.
214
+ - `.gitignore` now excludes `template/`, `test_docs/`, and `.worktrees/`.
215
+
216
+ ### Carried forward unchanged
217
+ - All v0.7 CLI flags (`--input-dir`, `--output-dir`, `--force`,
218
+ `--strip-links`, `--recursive`, `--max-file-size`, positional file/URL/dir
219
+ arguments). The `--strip-links` flag is now wired through `PipelineOptions`.
220
+ - SSRF protection on URL fetching (private/reserved/loopback IP rejection).
221
+ - File size limits (default 100 MB).
222
+ - Filename sanitization rules.
223
+ - Skip-if-exists unless `--force`.
224
+ - pymupdf4llm, mammoth, markdownify, trafilatura, BeautifulSoup, lxml
225
+ backends.
226
+ - Python 3.10+ floor.
227
+
228
+ ### Out of scope for Phase 1
229
+ Phase 2 (Docling backend, `--high-fidelity`, structured-lane stages S1–S4),
230
+ Phase 3 (figure/OCR handling, text-lane stages T1–T6, full metadata
231
+ extraction), Phase 4 (`--meta`, `.any2md.toml`, `--auto-id`, `--strict`,
232
+ `--quiet`, `--verbose`, full documentation rewrite), Phase 5 (release
233
+ 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.