adeu 3.2.0__tar.gz → 3.2.2__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 (273) hide show
  1. {adeu-3.2.0 → adeu-3.2.2}/PKG-INFO +11 -5
  2. {adeu-3.2.0 → adeu-3.2.2}/README.md +10 -4
  3. {adeu-3.2.0 → adeu-3.2.2}/pyproject.toml +1 -1
  4. {adeu-3.2.0 → adeu-3.2.2}/server.json +2 -2
  5. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/cli.py +50 -10
  6. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/compare.py +14 -4
  7. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/disk_cache.py +5 -0
  8. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/ingest.py +47 -2
  9. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/_response_builders.py +1 -1
  10. adeu-3.2.2/src/adeu/mcp_components/build_info.json +1 -0
  11. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/tools/document.py +29 -17
  12. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/tools/live_word.py +13 -1
  13. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/models.py +21 -6
  14. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/outline.py +48 -28
  15. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/redline/engine.py +59 -7
  16. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/redline/mapper.py +144 -36
  17. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/sanitize/core.py +3 -3
  18. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/serve.py +20 -2
  19. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/text_revision.py +40 -1
  20. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/docx.py +441 -80
  21. {adeu-3.2.0 → adeu-3.2.2}/tests/test_gemini_schema_compat.py +1 -0
  22. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_2026_07_18.py +1 -1
  23. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v5.py +2 -2
  24. adeu-3.2.2/tests/test_resolved_numbering_and_extract_output.py +438 -0
  25. {adeu-3.2.0 → adeu-3.2.2}/tests/test_serve_daemon.py +59 -0
  26. {adeu-3.2.0 → adeu-3.2.2}/tests/test_tool_description_workflows.py +12 -0
  27. {adeu-3.2.0 → adeu-3.2.2}/uv.lock +1 -1
  28. adeu-3.2.0/src/adeu/mcp_components/build_info.json +0 -1
  29. {adeu-3.2.0 → adeu-3.2.2}/.gitignore +0 -0
  30. {adeu-3.2.0 → adeu-3.2.2}/Dockerfile +0 -0
  31. {adeu-3.2.0 → adeu-3.2.2}/LICENSE +0 -0
  32. {adeu-3.2.0 → adeu-3.2.2}/scripts/abstract_xml.py +0 -0
  33. {adeu-3.2.0 → adeu-3.2.2}/scripts/debug_rels.py +0 -0
  34. {adeu-3.2.0 → adeu-3.2.2}/scripts/debug_structure.py +0 -0
  35. {adeu-3.2.0 → adeu-3.2.2}/scripts/debug_styles.py +0 -0
  36. {adeu-3.2.0 → adeu-3.2.2}/scripts/debug_xml.py +0 -0
  37. {adeu-3.2.0 → adeu-3.2.2}/scripts/generate_goldens.py +0 -0
  38. {adeu-3.2.0 → adeu-3.2.2}/scripts/golden_projection.py +0 -0
  39. {adeu-3.2.0 → adeu-3.2.2}/scripts/inspect_test_docx.py +0 -0
  40. {adeu-3.2.0 → adeu-3.2.2}/scripts/login_staging.py +0 -0
  41. {adeu-3.2.0 → adeu-3.2.2}/scripts/reproduce_issue.py +0 -0
  42. {adeu-3.2.0 → adeu-3.2.2}/scripts/test_numbered_list.py +0 -0
  43. {adeu-3.2.0 → adeu-3.2.2}/scripts/verify_staging_polling.py +0 -0
  44. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/__init__.py +0 -0
  45. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/assets/adeu.svg +0 -0
  46. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/assets/logo.png +0 -0
  47. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/assets/marked.min.js +0 -0
  48. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/diff.py +0 -0
  49. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/domain.py +0 -0
  50. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/fields.py +0 -0
  51. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/markup.py +0 -0
  52. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/__init__.py +0 -0
  53. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/doc_cache.py +0 -0
  54. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/resources/markdown_ui.py +0 -0
  55. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/shared.py +0 -0
  56. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/tools/live_word_ops.py +0 -0
  57. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/mcp_components/tools/sanitize.py +0 -0
  58. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/pagination.py +0 -0
  59. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/payloads.py +0 -0
  60. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/redline/comments.py +0 -0
  61. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/redline/gates.py +0 -0
  62. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/sanitize/__init__.py +0 -0
  63. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/sanitize/report.py +0 -0
  64. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/sanitize/transforms.py +0 -0
  65. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/server.py +0 -0
  66. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/templates/markdown_ui.html +0 -0
  67. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/console.py +0 -0
  68. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/content_controls.py +0 -0
  69. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/field_write.py +0 -0
  70. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/long_hex_number.py +0 -0
  71. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/opc.py +0 -0
  72. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/protection.py +0 -0
  73. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/safe_regex.py +0 -0
  74. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/schema_order.py +0 -0
  75. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/text.py +0 -0
  76. {adeu-3.2.0 → adeu-3.2.2}/src/adeu/utils/xml_debug.py +0 -0
  77. {adeu-3.2.0 → adeu-3.2.2}/tests/__init__.py +0 -0
  78. {adeu-3.2.0 → adeu-3.2.2}/tests/cc_fixture.py +0 -0
  79. {adeu-3.2.0 → adeu-3.2.2}/tests/conftest.py +0 -0
  80. {adeu-3.2.0 → adeu-3.2.2}/tests/docx_fixtures.py +0 -0
  81. {adeu-3.2.0 → adeu-3.2.2}/tests/fixtures_synth.py +0 -0
  82. {adeu-3.2.0 → adeu-3.2.2}/tests/golden_manifest.txt +0 -0
  83. {adeu-3.2.0 → adeu-3.2.2}/tests/repro_golden.py +0 -0
  84. {adeu-3.2.0 → adeu-3.2.2}/tests/sdt_fixtures.py +0 -0
  85. {adeu-3.2.0 → adeu-3.2.2}/tests/test_atomic_batch_pipeline.py +0 -0
  86. {adeu-3.2.0 → adeu-3.2.2}/tests/test_author_impersonation_gate.py +0 -0
  87. {adeu-3.2.0 → adeu-3.2.2}/tests/test_batch_ergonomics_and_receipts.py +0 -0
  88. {adeu-3.2.0 → adeu-3.2.2}/tests/test_batch_reliability.py +0 -0
  89. {adeu-3.2.0 → adeu-3.2.2}/tests/test_batch_validation.py +0 -0
  90. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_anchor_chrome_protection.py +0 -0
  91. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_anchor_fabrication_refusal.py +0 -0
  92. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_checkboxes.py +0 -0
  93. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_fields_ledger.py +0 -0
  94. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_fields_surfaces.py +0 -0
  95. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_gate_surfaces.py +0 -0
  96. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_gates.py +0 -0
  97. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_projection_inline.py +0 -0
  98. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_protection_state.py +0 -0
  99. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_run_control_identity.py +0 -0
  100. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_set_field.py +0 -0
  101. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_span_control_identity.py +0 -0
  102. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cc_widening_walls.py +0 -0
  103. {adeu-3.2.0 → adeu-3.2.2}/tests/test_changes_ledger.py +0 -0
  104. {adeu-3.2.0 → adeu-3.2.2}/tests/test_clean_view.py +0 -0
  105. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cli_bug_repro.py +0 -0
  106. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cli_encoding.py +0 -0
  107. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cli_features.py +0 -0
  108. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cli_init.py +0 -0
  109. {adeu-3.2.0 → adeu-3.2.2}/tests/test_comment_list.py +0 -0
  110. {adeu-3.2.0 → adeu-3.2.2}/tests/test_comment_xml_structure.py +0 -0
  111. {adeu-3.2.0 → adeu-3.2.2}/tests/test_comments.py +0 -0
  112. {adeu-3.2.0 → adeu-3.2.2}/tests/test_comments_threading.py +0 -0
  113. {adeu-3.2.0 → adeu-3.2.2}/tests/test_compact_diff_json.py +0 -0
  114. {adeu-3.2.0 → adeu-3.2.2}/tests/test_content_control_classification.py +0 -0
  115. {adeu-3.2.0 → adeu-3.2.2}/tests/test_context_trimming.py +0 -0
  116. {adeu-3.2.0 → adeu-3.2.2}/tests/test_corpus_validation.py +0 -0
  117. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cross_platform_consistency.py +0 -0
  118. {adeu-3.2.0 → adeu-3.2.2}/tests/test_cross_platform_integrity.py +0 -0
  119. {adeu-3.2.0 → adeu-3.2.2}/tests/test_diff_logic.py +0 -0
  120. {adeu-3.2.0 → adeu-3.2.2}/tests/test_diff_subalignment_and_balance.py +0 -0
  121. {adeu-3.2.0 → adeu-3.2.2}/tests/test_disk_projection_cache.py +0 -0
  122. {adeu-3.2.0 → adeu-3.2.2}/tests/test_doc_cache.py +0 -0
  123. {adeu-3.2.0 → adeu-3.2.2}/tests/test_document_settings_warnings.py +0 -0
  124. {adeu-3.2.0 → adeu-3.2.2}/tests/test_domain_semantics.py +0 -0
  125. {adeu-3.2.0 → adeu-3.2.2}/tests/test_edit_report_index.py +0 -0
  126. {adeu-3.2.0 → adeu-3.2.2}/tests/test_explicit_salvage.py +0 -0
  127. {adeu-3.2.0 → adeu-3.2.2}/tests/test_failure_envelope.py +0 -0
  128. {adeu-3.2.0 → adeu-3.2.2}/tests/test_failure_recovery_protocol.py +0 -0
  129. {adeu-3.2.0 → adeu-3.2.2}/tests/test_fastmcp3_compat.py +0 -0
  130. {adeu-3.2.0 → adeu-3.2.2}/tests/test_file_hints_and_id_discovery.py +0 -0
  131. {adeu-3.2.0 → adeu-3.2.2}/tests/test_fixtures_synth.py +0 -0
  132. {adeu-3.2.0 → adeu-3.2.2}/tests/test_fixtures_verification.py +0 -0
  133. {adeu-3.2.0 → adeu-3.2.2}/tests/test_flat_opc_to_docx.py +0 -0
  134. {adeu-3.2.0 → adeu-3.2.2}/tests/test_formatting.py +0 -0
  135. {adeu-3.2.0 → adeu-3.2.2}/tests/test_fused_json_hint.py +0 -0
  136. {adeu-3.2.0 → adeu-3.2.2}/tests/test_fuzzy_matching.py +0 -0
  137. {adeu-3.2.0 → adeu-3.2.2}/tests/test_fuzzy_whitespace_context.py +0 -0
  138. {adeu-3.2.0 → adeu-3.2.2}/tests/test_guard_message.py +0 -0
  139. {adeu-3.2.0 → adeu-3.2.2}/tests/test_header_block_guard.py +0 -0
  140. {adeu-3.2.0 → adeu-3.2.2}/tests/test_ingest_comments.py +0 -0
  141. {adeu-3.2.0 → adeu-3.2.2}/tests/test_json_unicode.py +0 -0
  142. {adeu-3.2.0 → adeu-3.2.2}/tests/test_lazy_snapshot.py +0 -0
  143. {adeu-3.2.0 → adeu-3.2.2}/tests/test_libreoffice_probe_isolation.py +0 -0
  144. {adeu-3.2.0 → adeu-3.2.2}/tests/test_live_word.py +0 -0
  145. {adeu-3.2.0 → adeu-3.2.2}/tests/test_live_word_content_controls.py +0 -0
  146. {adeu-3.2.0 → adeu-3.2.2}/tests/test_live_word_dispatch.py +0 -0
  147. {adeu-3.2.0 → adeu-3.2.2}/tests/test_live_word_gate_agreement.py +0 -0
  148. {adeu-3.2.0 → adeu-3.2.2}/tests/test_live_word_para_id_signed_int32.py +0 -0
  149. {adeu-3.2.0 → adeu-3.2.2}/tests/test_live_word_structured_insertion.py +0 -0
  150. {adeu-3.2.0 → adeu-3.2.2}/tests/test_local_script_paths.py +0 -0
  151. {adeu-3.2.0 → adeu-3.2.2}/tests/test_manual_markup_mode.py +0 -0
  152. {adeu-3.2.0 → adeu-3.2.2}/tests/test_marker_boundary_insert.py +0 -0
  153. {adeu-3.2.0 → adeu-3.2.2}/tests/test_markup.py +0 -0
  154. {adeu-3.2.0 → adeu-3.2.2}/tests/test_markup_fixes.py +0 -0
  155. {adeu-3.2.0 → adeu-3.2.2}/tests/test_mcp_apply_text_revision.py +0 -0
  156. {adeu-3.2.0 → adeu-3.2.2}/tests/test_mcp_diff_output.py +0 -0
  157. {adeu-3.2.0 → adeu-3.2.2}/tests/test_mcp_reasoning_optional.py +0 -0
  158. {adeu-3.2.0 → adeu-3.2.2}/tests/test_modern_comments.py +0 -0
  159. {adeu-3.2.0 → adeu-3.2.2}/tests/test_multi_run_comment_coalescing.py +0 -0
  160. {adeu-3.2.0 → adeu-3.2.2}/tests/test_natural_diffs.py +0 -0
  161. {adeu-3.2.0 → adeu-3.2.2}/tests/test_nested_markdown.py +0 -0
  162. {adeu-3.2.0 → adeu-3.2.2}/tests/test_no_chrome.py +0 -0
  163. {adeu-3.2.0 → adeu-3.2.2}/tests/test_onestep_compare.py +0 -0
  164. {adeu-3.2.0 → adeu-3.2.2}/tests/test_ooxml_schema_compliance.py +0 -0
  165. {adeu-3.2.0 → adeu-3.2.2}/tests/test_opc_document_types.py +0 -0
  166. {adeu-3.2.0 → adeu-3.2.2}/tests/test_outline_fast_equivalence.py +0 -0
  167. {adeu-3.2.0 → adeu-3.2.2}/tests/test_page_ranges.py +0 -0
  168. {adeu-3.2.0 → adeu-3.2.2}/tests/test_pagination_scanner_equivalence.py +0 -0
  169. {adeu-3.2.0 → adeu-3.2.2}/tests/test_paragraph_split_reject_parity.py +0 -0
  170. {adeu-3.2.0 → adeu-3.2.2}/tests/test_projection_goldens.py +0 -0
  171. {adeu-3.2.0 → adeu-3.2.2}/tests/test_properties.py +0 -0
  172. {adeu-3.2.0 → adeu-3.2.2}/tests/test_property_invariants.py +0 -0
  173. {adeu-3.2.0 → adeu-3.2.2}/tests/test_pure_comment_disk.py +0 -0
  174. {adeu-3.2.0 → adeu-3.2.2}/tests/test_read_ergonomics.py +0 -0
  175. {adeu-3.2.0 → adeu-3.2.2}/tests/test_regressions.py +0 -0
  176. {adeu-3.2.0 → adeu-3.2.2}/tests/test_reject_and_nested_redline.py +0 -0
  177. {adeu-3.2.0 → adeu-3.2.2}/tests/test_report_minimal.py +0 -0
  178. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_accept_all_changes_leak.py +0 -0
  179. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_accept_all_table_row_loss.py +0 -0
  180. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_accept_data_loss.py +0 -0
  181. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_batch_5.py +0 -0
  182. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_benchmark_schema_failures.py +0 -0
  183. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_block_marker_paragraph_parity.py +0 -0
  184. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_boilerplate_structure.py +0 -0
  185. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_chg_id_coalescing.py +0 -0
  186. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_comment_fragment_reject.py +0 -0
  187. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_comment_reference_style.py +0 -0
  188. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_comment_threading_anchoring_typography.py +0 -0
  189. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_comments_cleanup.py +0 -0
  190. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_comments_namespace_fix.py +0 -0
  191. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_customxml_missing_part.py +0 -0
  192. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_feedback_layer.py +0 -0
  193. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_feedback_observations.py +0 -0
  194. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_foreign_comment_range_modify.py +0 -0
  195. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_formatting_bugs.py +0 -0
  196. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_fte_2026_07_16.py +0 -0
  197. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_fte_2026_07_16_run2.py +0 -0
  198. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_fuzzy_regex_timeout.py +0 -0
  199. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_heading_bug.py +0 -0
  200. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_heading_section_insert.py +0 -0
  201. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_id_pool_renumber.py +0 -0
  202. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_indexed_edits.py +0 -0
  203. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_insertion_anchor.py +0 -0
  204. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_invisible_replies.py +0 -0
  205. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_issue157_tab_preservation.py +0 -0
  206. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_issue158_paragraph_delete_reject.py +0 -0
  207. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_issue159_comment_timestamp.py +0 -0
  208. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_issue23.py +0 -0
  209. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_issue_114_cross_part_revisions.py +0 -0
  210. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_list_matching.py +0 -0
  211. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_live_run_fragmentation.py +0 -0
  212. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_markdown_boundary.py +0 -0
  213. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_markdown_newline.py +0 -0
  214. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_marker_preserving_edits.py +0 -0
  215. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_multilevel_lists.py +0 -0
  216. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_multirun_deletion_ids.py +0 -0
  217. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_nested_corruption.py +0 -0
  218. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_nested_insertions.py +0 -0
  219. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_nested_table_row_cell_leakage.py +0 -0
  220. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_para_id_signed_int32.py +0 -0
  221. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_projection_parity_gaps.py +0 -0
  222. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_customer_assessment_2026_07_23.py +0 -0
  223. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_issues_v2.py +0 -0
  224. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_engine.py +0 -0
  225. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_mcp.py +0 -0
  226. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_rendering.py +0 -0
  227. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_reports.py +0 -0
  228. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_mcp_issues_2026_07_20.py +0 -0
  229. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report.py +0 -0
  230. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_cli_2026_07_22.py +0 -0
  231. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v2.py +0 -0
  232. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v3.py +0 -0
  233. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v4.py +0 -0
  234. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v6.py +0 -0
  235. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v7.py +0 -0
  236. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v8.py +0 -0
  237. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_report_v9.py +0 -0
  238. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_qa_round3_2026_07_24.py +0 -0
  239. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_raw_ooxml_in_projection.py +0 -0
  240. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_redline_replay_wrong_document.py +0 -0
  241. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_rejected_batch_action_leak.py +0 -0
  242. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_report_boundary_failures.py +0 -0
  243. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_resolved_comment_marker_parity.py +0 -0
  244. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_round16_bugs.py +0 -0
  245. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_sdt_table_row_cell_invisibility.py +0 -0
  246. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_stranded_comment_anchor.py +0 -0
  247. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_surgical_word_diff.py +0 -0
  248. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_table_regressions.py +0 -0
  249. {adeu-3.2.0 → adeu-3.2.2}/tests/test_repro_v4_report.py +0 -0
  250. {adeu-3.2.0 → adeu-3.2.2}/tests/test_response_budget_guard.py +0 -0
  251. {adeu-3.2.0 → adeu-3.2.2}/tests/test_roundtrip.py +0 -0
  252. {adeu-3.2.0 → adeu-3.2.2}/tests/test_run_fusion_equivalence.py +0 -0
  253. {adeu-3.2.0 → adeu-3.2.2}/tests/test_safety.py +0 -0
  254. {adeu-3.2.0 → adeu-3.2.2}/tests/test_sanitize.py +0 -0
  255. {adeu-3.2.0 → adeu-3.2.2}/tests/test_search_paging.py +0 -0
  256. {adeu-3.2.0 → adeu-3.2.2}/tests/test_search_write_engine.py +0 -0
  257. {adeu-3.2.0 → adeu-3.2.2}/tests/test_server.py +0 -0
  258. {adeu-3.2.0 → adeu-3.2.2}/tests/test_source_encoding.py +0 -0
  259. {adeu-3.2.0 → adeu-3.2.2}/tests/test_stringified_json_search_page.py +0 -0
  260. {adeu-3.2.0 → adeu-3.2.2}/tests/test_structural_edits.py +0 -0
  261. {adeu-3.2.0 → adeu-3.2.2}/tests/test_style_cache_optimization.py +0 -0
  262. {adeu-3.2.0 → adeu-3.2.2}/tests/test_styling_and_lists.py +0 -0
  263. {adeu-3.2.0 → adeu-3.2.2}/tests/test_substitution_cluster.py +0 -0
  264. {adeu-3.2.0 → adeu-3.2.2}/tests/test_table_creation.py +0 -0
  265. {adeu-3.2.0 → adeu-3.2.2}/tests/test_tables.py +0 -0
  266. {adeu-3.2.0 → adeu-3.2.2}/tests/test_terse_errors.py +0 -0
  267. {adeu-3.2.0 → adeu-3.2.2}/tests/test_twin_projection_parity.py +0 -0
  268. {adeu-3.2.0 → adeu-3.2.2}/tests/test_whole_paragraph_delete_reject.py +0 -0
  269. {adeu-3.2.0 → adeu-3.2.2}/tests/test_workflow_blocking.py +0 -0
  270. {adeu-3.2.0 → adeu-3.2.2}/tests/test_xml_debug.py +0 -0
  271. {adeu-3.2.0 → adeu-3.2.2}/tests/utils.py +0 -0
  272. {adeu-3.2.0 → adeu-3.2.2}/tests/verify_sanitized.py +0 -0
  273. {adeu-3.2.0 → adeu-3.2.2}/tests/word_com.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: adeu
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: Automated DOCX Redlining Engine
5
5
  Project-URL: Homepage, https://adeu.ai
6
6
  Project-URL: Repository, https://github.com/dealfluence/adeu
@@ -93,11 +93,14 @@ uvx adeu extract --live
93
93
  ```
94
94
 
95
95
  ### Diffing
96
- Generate a word-level patch diff between two document versions.
96
+ Generate a word-level patch diff or a one-step redlined `.docx` comparison between two document versions.
97
97
  ```bash
98
- # Compare two DOCX files
98
+ # Compare two DOCX files (@@ Word Patch @@ output)
99
99
  uvx adeu diff original.docx modified.docx
100
100
 
101
+ # Write the comparison directly as a tracked-changes .docx in one step
102
+ uvx adeu diff original.docx modified.docx -o redlined.docx --author "AI Reviewer"
103
+
101
104
  # Output raw JSON edits for programmatic use
102
105
  uvx adeu diff original.docx modified.docx --json
103
106
  ```
@@ -108,6 +111,9 @@ Apply a JSON array of `DocumentChange` objects (or a modified markdown file) bac
108
111
  # Apply a JSON batch of edits to a file
109
112
  uvx adeu apply original.docx edits.json --author "AI Reviewer" -o redlined.docx
110
113
 
114
+ # Manual / inline formatting markup (strikethrough + bold red underline instead of Track Changes)
115
+ uvx adeu apply original.docx edits.json --author "AI Reviewer" --manual -o manual.docx
116
+
111
117
  # Emit the batch result as machine-readable JSON on stdout (for agents/scripts)
112
118
  uvx adeu apply original.docx edits.json --json
113
119
 
@@ -173,14 +179,14 @@ The I/O contract (see `docs/cli-agent-spec.md` for the full specification):
173
179
  - `0` = success (in `--partial` mode, at least one edit applied; check `edits_skipped` in JSON stats to detect skipped edits)
174
180
  - `1` = failure (in atomic mode, if any edit fails; in `--partial` mode, if all edits fail)
175
181
 
176
- `adeu apply --json` prints the engine's raw stats object — `edits_applied`, `edits_skipped`, per-edit reports with CriticMarkup previews, plus `output_path` — and suppresses the human-readable logs. A batch that fails validation prints `{"error": "batch_validation_failed", "errors": [...]}` and exits 1.
182
+ `adeu apply --json` prints the engine's raw stats object — `edits_applied`, `edits_skipped`, per-edit reports with CriticMarkup previews, plus `output_path` — and suppresses the human-readable logs. A batch that fails validation prints `{"error": "batch_validation_failed", "failed": [{"index", "reason"}, ...], "errors": [...]}` and exits 1.
177
183
 
178
184
  ## The Python SDK
179
185
 
180
186
  The SDK allows you to embed Adeu's Redline Engine directly into your own Python applications.
181
187
 
182
188
  ### Applying Tracked Changes
183
- The engine processes a flat list of `DocumentChange` objects (`ModifyText`, `AcceptChange`, `RejectChange`, `ReplyComment`, `InsertTableRow`, `DeleteTableRow`).
189
+ The engine processes a flat list of `DocumentChange` objects (`ModifyText`, `SetField`, `AcceptChange`, `RejectChange`, `ReplyComment`, `InsertTableRow`, `DeleteTableRow`).
184
190
 
185
191
  ```python
186
192
  from io import BytesIO
@@ -43,11 +43,14 @@ uvx adeu extract --live
43
43
  ```
44
44
 
45
45
  ### Diffing
46
- Generate a word-level patch diff between two document versions.
46
+ Generate a word-level patch diff or a one-step redlined `.docx` comparison between two document versions.
47
47
  ```bash
48
- # Compare two DOCX files
48
+ # Compare two DOCX files (@@ Word Patch @@ output)
49
49
  uvx adeu diff original.docx modified.docx
50
50
 
51
+ # Write the comparison directly as a tracked-changes .docx in one step
52
+ uvx adeu diff original.docx modified.docx -o redlined.docx --author "AI Reviewer"
53
+
51
54
  # Output raw JSON edits for programmatic use
52
55
  uvx adeu diff original.docx modified.docx --json
53
56
  ```
@@ -58,6 +61,9 @@ Apply a JSON array of `DocumentChange` objects (or a modified markdown file) bac
58
61
  # Apply a JSON batch of edits to a file
59
62
  uvx adeu apply original.docx edits.json --author "AI Reviewer" -o redlined.docx
60
63
 
64
+ # Manual / inline formatting markup (strikethrough + bold red underline instead of Track Changes)
65
+ uvx adeu apply original.docx edits.json --author "AI Reviewer" --manual -o manual.docx
66
+
61
67
  # Emit the batch result as machine-readable JSON on stdout (for agents/scripts)
62
68
  uvx adeu apply original.docx edits.json --json
63
69
 
@@ -123,14 +129,14 @@ The I/O contract (see `docs/cli-agent-spec.md` for the full specification):
123
129
  - `0` = success (in `--partial` mode, at least one edit applied; check `edits_skipped` in JSON stats to detect skipped edits)
124
130
  - `1` = failure (in atomic mode, if any edit fails; in `--partial` mode, if all edits fail)
125
131
 
126
- `adeu apply --json` prints the engine's raw stats object — `edits_applied`, `edits_skipped`, per-edit reports with CriticMarkup previews, plus `output_path` — and suppresses the human-readable logs. A batch that fails validation prints `{"error": "batch_validation_failed", "errors": [...]}` and exits 1.
132
+ `adeu apply --json` prints the engine's raw stats object — `edits_applied`, `edits_skipped`, per-edit reports with CriticMarkup previews, plus `output_path` — and suppresses the human-readable logs. A batch that fails validation prints `{"error": "batch_validation_failed", "failed": [{"index", "reason"}, ...], "errors": [...]}` and exits 1.
127
133
 
128
134
  ## The Python SDK
129
135
 
130
136
  The SDK allows you to embed Adeu's Redline Engine directly into your own Python applications.
131
137
 
132
138
  ### Applying Tracked Changes
133
- The engine processes a flat list of `DocumentChange` objects (`ModifyText`, `AcceptChange`, `RejectChange`, `ReplyComment`, `InsertTableRow`, `DeleteTableRow`).
139
+ The engine processes a flat list of `DocumentChange` objects (`ModifyText`, `SetField`, `AcceptChange`, `RejectChange`, `ReplyComment`, `InsertTableRow`, `DeleteTableRow`).
134
140
 
135
141
  ```python
136
142
  from io import BytesIO
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adeu"
3
- version = "3.2.0"
3
+ version = "3.2.2"
4
4
  description = "Automated DOCX Redlining Engine"
5
5
  authors = [
6
6
  { name = "Mikko Korpela" },
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/dealfluence/adeu",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.2.0",
9
+ "version": "3.2.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@adeu/mcp-server",
14
- "version": "3.2.0",
14
+ "version": "3.2.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
@@ -830,7 +830,13 @@ def handle_extract(args):
830
830
  # modes it must be a positive integer; anything else is a hard error,
831
831
  # never a silent fallback to page 1 (QA L1).
832
832
  page_num = 1
833
- want_all_pages = False
833
+ # Writing to a file (`-o <path>`, where `-o -` was already normalized to
834
+ # None above) defaults to the full document when `--page` is omitted:
835
+ # synthetic pagination protects stdout/LLM context windows, whereas file
836
+ # extraction is expected to contain the whole document.
837
+ want_all_pages = bool(
838
+ args.page is None and args.output and args.mode == "full" and not getattr(args, "search_query", None)
839
+ )
834
840
  is_page_range = False
835
841
  range_start = 1
836
842
  range_end = 1
@@ -1064,7 +1070,29 @@ def handle_extract(args):
1064
1070
  # extracted text otherwise. stdout stays quiet either way — printing
1065
1071
  # the full payload again surprised pipeline users (QA 2026-07-18 L4).
1066
1072
  _write_output_or_exit(args.output, json_output if json_output is not None else output_text)
1067
- print(f"Extracted {'JSON' if json_output is not None else 'text'} to {args.output}", file=sys.stderr)
1073
+ status_msg = f"Extracted {'JSON' if json_output is not None else 'text'} to {args.output}"
1074
+ if not getattr(args, "search_query", None) and args.mode == "full" and not want_all_pages:
1075
+ from adeu.pagination import PAGE_RANGE_MAX_PAGES, paginate, split_structural_appendix
1076
+
1077
+ total_pages = paginate(split_structural_appendix(text)[0], "").total_pages
1078
+ if is_page_range:
1079
+ shown_end = min(range_end, range_start + PAGE_RANGE_MAX_PAGES - 1, total_pages)
1080
+ if range_start > 1 or shown_end < total_pages:
1081
+ status_msg = (
1082
+ f"Extracted pages {range_start}-{shown_end} of {total_pages} to {args.output} "
1083
+ "(partial extract; pass --page all for the full document)"
1084
+ )
1085
+ elif total_pages > 1:
1086
+ status_msg = (
1087
+ f"Extracted page {page_num} of {total_pages} to {args.output} "
1088
+ "(partial extract; pass --page all for the full document)"
1089
+ )
1090
+ elif args.mode == "appendix" and "Continues on appendix page " in output_text:
1091
+ status_msg = (
1092
+ f"Extracted appendix page {page_num} to {args.output} "
1093
+ "(partial appendix extract; pass --page N for subsequent pages)"
1094
+ )
1095
+ print(status_msg, file=sys.stderr)
1068
1096
  elif json_output is not None:
1069
1097
  print(json_output)
1070
1098
  else:
@@ -1203,11 +1231,19 @@ def handle_diff(args):
1203
1231
  try:
1204
1232
  doc_orig = _load_docx_or_exit(args.original)
1205
1233
  text_orig, struct_orig = _extract_text_from_doc(
1206
- doc_orig, clean_view=compare_clean, include_appendix=False, return_structure=True
1234
+ doc_orig,
1235
+ clean_view=compare_clean,
1236
+ include_appendix=False,
1237
+ return_structure=True,
1238
+ resolve_numbering=False,
1207
1239
  )
1208
1240
  doc_mod = _load_docx_or_exit(args.modified)
1209
1241
  text_mod, struct_mod = _extract_text_from_doc(
1210
- doc_mod, clean_view=compare_clean, include_appendix=False, return_structure=True
1242
+ doc_mod,
1243
+ clean_view=compare_clean,
1244
+ include_appendix=False,
1245
+ return_structure=True,
1246
+ resolve_numbering=False,
1211
1247
  )
1212
1248
  except SystemExit:
1213
1249
  raise
@@ -2114,7 +2150,12 @@ def _main_impl():
2114
2150
  action="store_true",
2115
2151
  help=f"{live_help_prefix}Extract text from live active Word document",
2116
2152
  )
2117
- p_extract.add_argument("-o", "--output", type=Path, help="Output file ('-' or omitted: stdout)")
2153
+ p_extract.add_argument(
2154
+ "-o",
2155
+ "--output",
2156
+ type=Path,
2157
+ help="Output file ('-' or omitted: stdout; defaults --page to 'all' in full mode when omitted)",
2158
+ )
2118
2159
  p_extract.add_argument(
2119
2160
  "--force",
2120
2161
  action="store_true",
@@ -2159,11 +2200,10 @@ def _main_impl():
2159
2200
  type=str,
2160
2201
  default=None,
2161
2202
  help=(
2162
- "Page number (1-indexed) for 'full' and 'appendix' modes (defaults to 1), "
2163
- "or 'all' to emit the entire document in one output (mode 'full' and search). "
2164
- "Use '--page all' when producing a text file for `adeu apply`/`adeu diff` — "
2165
- "a single page of a multi-page document cannot round-trip. Note: pages are "
2166
- "synthetic, length-based content chunks sized for LLM consumption — they do "
2203
+ "Page number (1-indexed) for 'full' and 'appendix' modes (defaults to 1 on stdout and 'all' "
2204
+ "when writing to -o <file>), or 'all' to emit the entire document in one output (mode 'full' "
2205
+ "and search). A single page of a multi-page document cannot round-trip into `adeu apply`/`adeu diff`. "
2206
+ "Note: pages are synthetic, length-based content chunks sized for LLM consumption — they do "
2167
2207
  "NOT correspond to printed Word pages or explicit page breaks."
2168
2208
  ),
2169
2209
  )
@@ -72,9 +72,11 @@ def refuse_input_alias(output_path: PathLike, original_path: PathLike, modified_
72
72
  )
73
73
 
74
74
 
75
- def apply_comparison_edits(original: bytes, edits: Sequence[DiffEdit], author: str) -> "tuple[bytes, int]":
75
+ def apply_comparison_edits(
76
+ original: bytes, edits: Sequence[DiffEdit], author: str, resolve_numbering: bool = False
77
+ ) -> "tuple[bytes, int]":
76
78
  """Applies diff-generated edits to `original` as tracked changes; all of them or none."""
77
- engine = RedlineEngine(BytesIO(original), author=author)
79
+ engine = RedlineEngine(BytesIO(original), author=author, resolve_numbering=resolve_numbering)
78
80
  applied, skipped = engine.apply_edits(list(edits)) # type: ignore[arg-type]
79
81
  if skipped:
80
82
  raise ComparisonError(
@@ -96,10 +98,18 @@ def build_redline_comparison(
96
98
  """Diffs two DOCX packages part by part and writes the result as a redline of `original`."""
97
99
  # Appendix excluded: its generated text is not writable content (QA 2026-07-18 H1).
98
100
  text_o, struct_o = _extract_text_from_doc(
99
- Document(BytesIO(original)), clean_view=compare_clean, include_appendix=False, return_structure=True
101
+ Document(BytesIO(original)),
102
+ clean_view=compare_clean,
103
+ include_appendix=False,
104
+ return_structure=True,
105
+ resolve_numbering=False,
100
106
  )
101
107
  text_m, struct_m = _extract_text_from_doc(
102
- Document(BytesIO(modified)), clean_view=compare_clean, include_appendix=False, return_structure=True
108
+ Document(BytesIO(modified)),
109
+ clean_view=compare_clean,
110
+ include_appendix=False,
111
+ return_structure=True,
112
+ resolve_numbering=False,
103
113
  )
104
114
  edits, warnings = generate_structured_edits(text_o, struct_o, text_m, struct_m) # type: ignore[arg-type]
105
115
  warnings = list(warnings) + collect_media_difference_warnings(original, modified)
@@ -9,6 +9,9 @@ from typing import Any, Dict, List, Optional, Tuple, Union
9
9
  from adeu import __version__
10
10
  from adeu.outline import OutlineNode
11
11
 
12
+ # Increment whenever Virtual Text projection rules change without a package version bump.
13
+ _CACHE_SCHEMA_VERSION = 2
14
+
12
15
 
13
16
  def get_default_cache_dir() -> Path:
14
17
  if env_dir := os.environ.get("ADEU_CACHE_DIR"):
@@ -125,6 +128,7 @@ class DiskProjectionCache:
125
128
  or key.get("mtime_ns") != mtime_ns
126
129
  or key.get("size") != size
127
130
  or key.get("version") != __version__
131
+ or key.get("schema") != _CACHE_SCHEMA_VERSION
128
132
  ):
129
133
  self._misses += 1
130
134
  return None
@@ -165,6 +169,7 @@ class DiskProjectionCache:
165
169
  "mtime_ns": mtime_ns,
166
170
  "size": size,
167
171
  "version": __version__,
172
+ "schema": _CACHE_SCHEMA_VERSION,
168
173
  }
169
174
 
170
175
  data: Dict[str, Any] = {"key": key_dict, "views": {}}
@@ -25,6 +25,7 @@ from adeu.utils.content_controls import (
25
25
  )
26
26
  from adeu.utils.docx import (
27
27
  DocxEvent,
28
+ NumberingTracker,
28
29
  ProjectedRun,
29
30
  _get_style_cache,
30
31
  apply_formatting_to_segments,
@@ -95,6 +96,7 @@ def extract_text_from_stream(
95
96
  filename: str = "document.docx",
96
97
  clean_view: bool = False,
97
98
  include_appendix: bool = True,
99
+ resolve_numbering: bool = True,
98
100
  ) -> str:
99
101
  """
100
102
  Extracts text from a file stream using raw run concatenation.
@@ -106,6 +108,9 @@ def extract_text_from_stream(
106
108
  include_appendix: If False, omits the generated read-only structural
107
109
  appendix — required whenever the text feeds a comparison
108
110
  (QA 2026-07-18 H1).
111
+ resolve_numbering: If True (default), emits resolved OOXML clause/list
112
+ numbers; if False, emits canonical '1. ' markers for
113
+ DOCX-to-DOCX comparison.
109
114
 
110
115
  CRITICAL: This must match DocumentMapper._build_map logic exactly.
111
116
  """
@@ -114,7 +119,12 @@ def extract_text_from_stream(
114
119
 
115
120
  sanitized_bytes = strip_bom_from_docx_bytes(file_stream.read())
116
121
  doc = Document(io.BytesIO(sanitized_bytes))
117
- return _extract_text_from_doc(doc, clean_view, include_appendix=include_appendix)
122
+ return _extract_text_from_doc(
123
+ doc,
124
+ clean_view,
125
+ include_appendix=include_appendix,
126
+ resolve_numbering=resolve_numbering,
127
+ )
118
128
  except Exception as e:
119
129
  logger.error(f"Text extraction failed: {e}", exc_info=True)
120
130
  raise ValueError(f"Could not extract text: {str(e)}") from e
@@ -126,6 +136,7 @@ def _extract_text_from_doc(
126
136
  include_appendix: bool = True,
127
137
  return_paragraph_offsets: bool = False,
128
138
  return_structure: bool = False,
139
+ resolve_numbering: bool = True,
129
140
  ):
130
141
  """
131
142
  Extracts text from an already-loaded python-docx Document.
@@ -177,6 +188,11 @@ def _extract_text_from_doc(
177
188
  # part_cursor accounts for the \n\n separator that will precede this part
178
189
  # in the final join, ensuring internal offsets align exactly.
179
190
  part_cursor = cursor + 2 if full_text else cursor
191
+ part_ref = getattr(part, "part", part)
192
+ part_style_cache, part_default_pstyle = _get_style_cache(part_ref)
193
+ numbering_tracker = NumberingTracker(
194
+ part_ref, style_cache=part_style_cache, resolve_numbering=resolve_numbering
195
+ )
180
196
  part_text = _extract_blocks(
181
197
  part,
182
198
  comments_map,
@@ -184,7 +200,12 @@ def _extract_text_from_doc(
184
200
  offset_map=offset_map,
185
201
  cursor=part_cursor,
186
202
  table_acc=structure.tables if structure is not None else None,
203
+ style_cache=part_style_cache,
204
+ default_pstyle=part_default_pstyle,
205
+ part=part_ref,
187
206
  sdt_infos=sdt_infos,
207
+ numbering_tracker=numbering_tracker,
208
+ resolve_numbering=resolve_numbering,
188
209
  )
189
210
  if part_text:
190
211
  if full_text:
@@ -223,6 +244,8 @@ def _extract_blocks(
223
244
  part: Any = None,
224
245
  sdt_infos: dict | None = None,
225
246
  in_cell: bool = False,
247
+ numbering_tracker: NumberingTracker | None = None,
248
+ resolve_numbering: bool = True,
226
249
  ) -> str:
227
250
  """
228
251
  Recursively extracts text from a container (Document, Cell, Header, etc.)
@@ -236,6 +259,8 @@ def _extract_blocks(
236
259
  part = getattr(container, "part", container)
237
260
  if style_cache is None:
238
261
  style_cache, default_pstyle = _get_style_cache(part)
262
+ if numbering_tracker is None:
263
+ numbering_tracker = NumberingTracker(part, style_cache=style_cache, resolve_numbering=resolve_numbering)
239
264
 
240
265
  blocks = []
241
266
  local_cursor = cursor
@@ -290,6 +315,8 @@ def _extract_blocks(
290
315
  default_pstyle=default_pstyle,
291
316
  part=part,
292
317
  in_cell=in_cell,
318
+ numbering_tracker=numbering_tracker,
319
+ resolve_numbering=resolve_numbering,
293
320
  )
294
321
  if inner:
295
322
  full = f"{open_tok}{inner}{close_tok}"
@@ -312,6 +339,9 @@ def _extract_blocks(
312
339
  sdt_infos=sdt_infos,
313
340
  style_cache=style_cache,
314
341
  default_pstyle=default_pstyle,
342
+ part=part,
343
+ numbering_tracker=numbering_tracker,
344
+ resolve_numbering=resolve_numbering,
315
345
  )
316
346
  if fn_text:
317
347
  blocks.append(fn_text)
@@ -324,7 +354,15 @@ def _extract_blocks(
324
354
  local_cursor -= 2
325
355
  elif isinstance(item, Paragraph):
326
356
  p_elem = item._element
327
- style_prefix = get_paragraph_prefix(p_elem, style_cache, default_pstyle, part=part)
357
+ num_snap = numbering_tracker.snapshot()
358
+ style_prefix = get_paragraph_prefix(
359
+ p_elem,
360
+ style_cache,
361
+ default_pstyle,
362
+ part=part,
363
+ numbering_tracker=numbering_tracker,
364
+ resolve_numbering=resolve_numbering,
365
+ )
328
366
  prefix = style_prefix
329
367
  if is_first_para and c_type == "FootnoteItem":
330
368
  prefix = f"[^{container.note_type}-{container.id}]: " + prefix
@@ -345,6 +383,7 @@ def _extract_blocks(
345
383
  # paragraph away; when nothing visible survives inside it,
346
384
  # the accepted view must not render an empty container
347
385
  # (QA round 3, finding 2.4).
386
+ numbering_tracker.restore(num_snap)
348
387
  if not is_first_block:
349
388
  local_cursor -= 2
350
389
  is_first_para = False
@@ -375,6 +414,8 @@ def _extract_blocks(
375
414
  style_cache=style_cache,
376
415
  default_pstyle=default_pstyle,
377
416
  part=part,
417
+ numbering_tracker=numbering_tracker,
418
+ resolve_numbering=resolve_numbering,
378
419
  )
379
420
  if table_text:
380
421
  blocks.append(table_text)
@@ -402,6 +443,8 @@ def extract_table(
402
443
  default_pstyle: str | None = None,
403
444
  part: Any = None,
404
445
  sdt_infos: dict | None = None,
446
+ numbering_tracker: NumberingTracker | None = None,
447
+ resolve_numbering: bool = True,
405
448
  ) -> str:
406
449
  """
407
450
  Args:
@@ -463,6 +506,8 @@ def extract_table(
463
506
  default_pstyle=default_pstyle,
464
507
  part=part,
465
508
  in_cell=True,
509
+ numbering_tracker=numbering_tracker,
510
+ resolve_numbering=resolve_numbering,
466
511
  )
467
512
  if cell_info is not None:
468
513
  # Cell-level control (sdtContent > w:tc): anchors render inline
@@ -714,7 +714,7 @@ def build_search_response(
714
714
  search_query: str,
715
715
  search_regex: bool,
716
716
  search_case_sensitive: bool,
717
- page: int | str | None,
717
+ page: int | str | tuple[int, int] | None,
718
718
  file_path: str,
719
719
  is_cli: bool = False,
720
720
  pagination_result: "PaginationResult | None" = None,
@@ -0,0 +1 @@
1
+ {"git_sha": "03500b9", "build_timestamp": "20261111103826"}
@@ -1004,10 +1004,18 @@ async def diff_docx_files(
1004
1004
  # diagnostics) is not document content — diffing it produces phantom
1005
1005
  # changes no apply can consume (QA 2026-07-18 H1).
1006
1006
  text_orig = extract_text_from_stream(
1007
- stream_orig, filename=Path(original_path).name, clean_view=compare_clean, include_appendix=False
1007
+ stream_orig,
1008
+ filename=Path(original_path).name,
1009
+ clean_view=compare_clean,
1010
+ include_appendix=False,
1011
+ resolve_numbering=False,
1008
1012
  )
1009
1013
  text_mod = extract_text_from_stream(
1010
- stream_mod, filename=Path(modified_path).name, clean_view=compare_clean, include_appendix=False
1014
+ stream_mod,
1015
+ filename=Path(modified_path).name,
1016
+ clean_view=compare_clean,
1017
+ include_appendix=False,
1018
+ resolve_numbering=False,
1011
1019
  )
1012
1020
  # atomic_criticmarkup: diff-display only — hunks must never cut into a
1013
1021
  # {--/{++/{>>/{== block (QA 2026-07-23 F15). No-op on clean text.
@@ -1381,11 +1389,14 @@ READ_DOCX_WIN32_EXTRA = (
1381
1389
 
1382
1390
  READ_DOCX_TAIL = (
1383
1391
  "Modes:\n"
1384
- "- 'full' (default): paginated body content. Use page=N to navigate.\n"
1392
+ "- 'full' (default): paginated body content. `page`: 1-indexed int, 'N-M' range (up to 8 pages; "
1393
+ "also filters `search_query`), or 'all' (serves first pages + L1 outline when over budget unless force=true).\n"
1385
1394
  "- 'outline': heading map only — start here for large docs to plan targeted reads. "
1386
1395
  "Defaults to L1-L2 headings; pass outline_max_level=3-6 to see deeper structure.\n"
1387
1396
  "- 'appendix': defined terms, anchors, and cross-reference targets. "
1388
- "Consult before editing legal/technical docs to avoid breaking references."
1397
+ "Consult before editing legal/technical docs to avoid breaking references.\n"
1398
+ "- 'changes': compact ledger of Tracked Changes (`Chg:N`) and Comments (`Com:N`).\n"
1399
+ "- 'fields': ledger of form fields (content controls) with `CC:<N>` IDs (paginate via `fields_offset`)."
1389
1400
  )
1390
1401
 
1391
1402
  PROCESS_BATCH_COMMON_DESC = (
@@ -1431,20 +1442,21 @@ PROCESS_BATCH_WIN32_EXTRA = (
1431
1442
  "Leave original_docx_path empty to edit whatever document is currently active.\n\n"
1432
1443
  )
1433
1444
  PROCESS_BATCH_OPERATIONS_DESC = (
1434
- "Each item in `changes` must specify a `type`:\n"
1445
+ "Changes by `type`:\n"
1435
1446
  "1. 'modify': Search-and-replace. `target_text` must uniquely match — add "
1436
- "context if ambiguous. `new_text` supports Markdown: '#'–'######' headings, "
1437
- "'**bold**', '_italic_', '\\n\\n' paragraph split. Empty `new_text` deletes. "
1438
- "Never write CriticMarkup ({++, {--, {>>) manually — use `comment`.\n"
1439
- "2. 'accept' / 'reject': Finalize or revert a tracked change by `target_id` (e.g. 'Chg:12'). "
1440
- "Ids are per package part: when one exists in several parts the bare id is refused; "
1441
- "add `part` (e.g. 'word/header1.xml').\n"
1442
- "3. 'reply': Reply to a comment by `target_id` (e.g. 'Com:5') with `text`.\n"
1443
- "4. 'set_field': Fill a content control (form field) — `field` is its 'CC:<N>' id, "
1444
- "tag or alias, `value` the text; list them with `read_docx` mode='fields'. Checkboxes "
1445
- "take true/false, dates YYYY-MM-DD, dropdowns a listed option. Data-bound controls "
1446
- "update their store too. A locked or protected control refuses and names the override "
1447
- "that permits it.\n"
1447
+ "context, or set `match_mode` ('first'|'all') or `regex=true`. Empty table "
1448
+ "cells emit `{#cell:<id>}` — pass that token as `target_text` to fill one. "
1449
+ "`new_text` supports '#'–'######' headings, '**bold**', '_italic_', '\\n\\n' "
1450
+ "paragraph split, and pipe tables (`| H1 | H2 |\\n|---|---|\\n| A | B |`). "
1451
+ "Empty `new_text` deletes. Never write CriticMarkup ({++, {--, {>>) — use "
1452
+ '`comment` (string or `[{"text": ..., "author": ...}]`).\n'
1453
+ "2. 'accept' / 'reject': Finalize or revert by `target_id` ('Chg:12'); add "
1454
+ "`part` ('word/header1.xml') when the same id exists in several parts.\n"
1455
+ "3. 'reply': Reply to `target_id` ('Com:5') with `text`.\n"
1456
+ "4. 'set_field': Fill a content control — `field` is 'CC:<N>', tag or alias, "
1457
+ "`value` the text (checkboxes true/false, dates YYYY-MM-DD, dropdowns a listed "
1458
+ "option; list via `read_docx` mode='fields'). Data-bound controls update their "
1459
+ "store; locked/protected controls refuse and name the override.\n"
1448
1460
  "5. 'insert_row' / 'delete_row': Table edits. Disk mode only — not supported on Live Word canvas.\n\n"
1449
1461
  "ID VOLATILITY: 'Chg:N'/'Com:N' shift between document states — call "
1450
1462
  "`read_docx` before any accept/reject/reply; never reuse earlier ids.\n\n"
@@ -382,13 +382,25 @@ if sys.platform == "win32":
382
382
  from adeu.mcp_components._response_builders import (
383
383
  build_search_response,
384
384
  )
385
+ from adeu.pagination import parse_page_arg as _parse_search_page
386
+
387
+ search_page: int | str | tuple[int, int] | None = page
388
+ if isinstance(page, str):
389
+ s_clean = page.strip().strip("'\"")
390
+ if "-" in s_clean:
391
+ try:
392
+ p_kind, p_val = _parse_search_page(s_clean)
393
+ if p_kind == "range":
394
+ search_page = p_val
395
+ except ValueError:
396
+ pass
385
397
 
386
398
  res = build_search_response(
387
399
  final_text,
388
400
  search_query,
389
401
  search_regex,
390
402
  search_case_sensitive,
391
- page,
403
+ search_page,
392
404
  actual_path,
393
405
  max_matches=max_matches,
394
406
  match_offset=match_offset,
@@ -199,8 +199,10 @@ class ModifyText(_EditState):
199
199
  target_text: str = Field(
200
200
  ...,
201
201
  description=(
202
- "Exact text to find. If the text appears multiple times (e.g. 'Fee'), include surrounding context. "
203
- "You can include CriticMarkup {==...==} in the target to match text inside existing markup."
202
+ "Exact text to find. If the text appears multiple times (e.g. 'Fee'), include surrounding context, "
203
+ "or set match_mode='first'/'all'. Empty table cells emit `{#cell:<id>}` — pass that token as "
204
+ "target_text to fill one. You can include CriticMarkup {==...==} in the target to match text inside "
205
+ "existing markup."
204
206
  ),
205
207
  )
206
208
 
@@ -208,7 +210,8 @@ class ModifyText(_EditState):
208
210
  ...,
209
211
  description=(
210
212
  "The desired text replacement. You may use Markdown formatting: "
211
- "'# Title' for headers, '**bold**' for bold, '_italic_' for italic. "
213
+ "'# Title' for headers, '**bold**' for bold, '_italic_' for italic, '\\n\\n' for paragraph breaks, "
214
+ "and pipe tables (`| Col 1 | Col 2 |\\n| --- | --- |\\n| A | B |`) to insert native Word tables. "
212
215
  "Do NOT manually write CriticMarkup tags ({++...++}, {--...--}, {>>...<<}, {==...==}). "
213
216
  "To add a comment, use the 'comment' parameter instead."
214
217
  ),
@@ -363,15 +366,18 @@ class FlatDocumentChange(BaseModel):
363
366
  target_text: Optional[str] = Field(
364
367
  None,
365
368
  description=(
366
- "Exact text to find. If the text appears multiple times (e.g. 'Fee'), include surrounding context. "
367
- "You can include CriticMarkup {==...==} in the target to match text inside existing markup."
369
+ "Exact text to find. If the text appears multiple times (e.g. 'Fee'), include surrounding context, "
370
+ "or set match_mode='first'/'all'. Empty table cells emit `{#cell:<id>}` — pass that token as "
371
+ "target_text to fill one. You can include CriticMarkup {==...==} in the target to match text inside "
372
+ "existing markup."
368
373
  ),
369
374
  )
370
375
  new_text: Optional[str] = Field(
371
376
  None,
372
377
  description=(
373
378
  "The desired text replacement. You may use Markdown formatting: "
374
- "'# Title' for headers, '**bold**' for bold, '_italic_' for italic. "
379
+ "'# Title' for headers, '**bold**' for bold, '_italic_' for italic, '\\n\\n' for paragraph breaks, "
380
+ "and pipe tables (`| Col 1 | Col 2 |\\n| --- | --- |\\n| A | B |`) to insert native Word tables. "
375
381
  "Do NOT manually write CriticMarkup tags ({++...++}, {--...--}, {>>...<<}, {==...==}). "
376
382
  "To add a comment, use the 'comment' parameter instead."
377
383
  ),
@@ -394,6 +400,15 @@ class FlatDocumentChange(BaseModel):
394
400
  None,
395
401
  description="The full ID string from the document text (e.g. 'Chg:12', 'Com:5').",
396
402
  )
403
+ part: Optional[str] = Field(
404
+ None,
405
+ description=(
406
+ "accept / reject only: OPC part holding the change, e.g. 'word/header1.xml'. "
407
+ "Revision ids are numbered per part, so the same Chg:N can name unrelated changes in "
408
+ "different parts; a bare ambiguous id is refused with an error listing the parts, and "
409
+ "this field picks one. Omit whenever the id is unique in the package."
410
+ ),
411
+ )
397
412
  text: Optional[str] = Field(
398
413
  None,
399
414
  description="The content of the reply body.",