adeu 3.2.2__tar.gz → 4.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 (276) hide show
  1. adeu-4.0.0/Dockerfile +12 -0
  2. {adeu-3.2.2 → adeu-4.0.0}/PKG-INFO +11 -6
  3. {adeu-3.2.2 → adeu-4.0.0}/README.md +4 -3
  4. {adeu-3.2.2 → adeu-4.0.0}/pyproject.toml +14 -3
  5. {adeu-3.2.2 → adeu-4.0.0}/server.json +2 -2
  6. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/cli.py +1 -1
  7. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/diff.py +129 -0
  8. adeu-4.0.0/src/adeu/mcp_components/build_info.json +1 -0
  9. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/document.py +32 -136
  10. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/live_word.py +13 -4
  11. adeu-4.0.0/src/adeu/server.py +348 -0
  12. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_init.py +1 -1
  13. adeu-4.0.0/tests/test_optional_mcp.py +238 -0
  14. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v7.py +1 -1
  15. {adeu-3.2.2 → adeu-4.0.0}/uv.lock +19 -5
  16. adeu-3.2.2/Dockerfile +0 -12
  17. adeu-3.2.2/src/adeu/mcp_components/build_info.json +0 -1
  18. adeu-3.2.2/src/adeu/server.py +0 -138
  19. {adeu-3.2.2 → adeu-4.0.0}/.gitignore +0 -0
  20. {adeu-3.2.2 → adeu-4.0.0}/LICENSE +0 -0
  21. {adeu-3.2.2 → adeu-4.0.0}/scripts/abstract_xml.py +0 -0
  22. {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_rels.py +0 -0
  23. {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_structure.py +0 -0
  24. {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_styles.py +0 -0
  25. {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_xml.py +0 -0
  26. {adeu-3.2.2 → adeu-4.0.0}/scripts/generate_goldens.py +0 -0
  27. {adeu-3.2.2 → adeu-4.0.0}/scripts/golden_projection.py +0 -0
  28. {adeu-3.2.2 → adeu-4.0.0}/scripts/inspect_test_docx.py +0 -0
  29. {adeu-3.2.2 → adeu-4.0.0}/scripts/login_staging.py +0 -0
  30. {adeu-3.2.2 → adeu-4.0.0}/scripts/reproduce_issue.py +0 -0
  31. {adeu-3.2.2 → adeu-4.0.0}/scripts/test_numbered_list.py +0 -0
  32. {adeu-3.2.2 → adeu-4.0.0}/scripts/verify_staging_polling.py +0 -0
  33. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/__init__.py +0 -0
  34. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/assets/adeu.svg +0 -0
  35. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/assets/logo.png +0 -0
  36. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/assets/marked.min.js +0 -0
  37. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/compare.py +0 -0
  38. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/disk_cache.py +0 -0
  39. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/domain.py +0 -0
  40. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/fields.py +0 -0
  41. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/ingest.py +0 -0
  42. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/markup.py +0 -0
  43. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/__init__.py +0 -0
  44. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/_response_builders.py +0 -0
  45. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/doc_cache.py +0 -0
  46. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/resources/markdown_ui.py +0 -0
  47. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/shared.py +0 -0
  48. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/live_word_ops.py +0 -0
  49. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/sanitize.py +0 -0
  50. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/models.py +0 -0
  51. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/outline.py +0 -0
  52. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/pagination.py +0 -0
  53. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/payloads.py +0 -0
  54. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/comments.py +0 -0
  55. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/engine.py +0 -0
  56. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/gates.py +0 -0
  57. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/mapper.py +0 -0
  58. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/__init__.py +0 -0
  59. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/core.py +0 -0
  60. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/report.py +0 -0
  61. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/transforms.py +0 -0
  62. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/serve.py +0 -0
  63. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/templates/markdown_ui.html +0 -0
  64. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/text_revision.py +0 -0
  65. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/console.py +0 -0
  66. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/content_controls.py +0 -0
  67. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/docx.py +0 -0
  68. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/field_write.py +0 -0
  69. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/long_hex_number.py +0 -0
  70. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/opc.py +0 -0
  71. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/protection.py +0 -0
  72. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/safe_regex.py +0 -0
  73. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/schema_order.py +0 -0
  74. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/text.py +0 -0
  75. {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/xml_debug.py +0 -0
  76. {adeu-3.2.2 → adeu-4.0.0}/tests/__init__.py +0 -0
  77. {adeu-3.2.2 → adeu-4.0.0}/tests/cc_fixture.py +0 -0
  78. {adeu-3.2.2 → adeu-4.0.0}/tests/conftest.py +0 -0
  79. {adeu-3.2.2 → adeu-4.0.0}/tests/docx_fixtures.py +0 -0
  80. {adeu-3.2.2 → adeu-4.0.0}/tests/fixtures_synth.py +0 -0
  81. {adeu-3.2.2 → adeu-4.0.0}/tests/golden_manifest.txt +0 -0
  82. {adeu-3.2.2 → adeu-4.0.0}/tests/repro_golden.py +0 -0
  83. {adeu-3.2.2 → adeu-4.0.0}/tests/sdt_fixtures.py +0 -0
  84. {adeu-3.2.2 → adeu-4.0.0}/tests/test_atomic_batch_pipeline.py +0 -0
  85. {adeu-3.2.2 → adeu-4.0.0}/tests/test_author_impersonation_gate.py +0 -0
  86. {adeu-3.2.2 → adeu-4.0.0}/tests/test_batch_ergonomics_and_receipts.py +0 -0
  87. {adeu-3.2.2 → adeu-4.0.0}/tests/test_batch_reliability.py +0 -0
  88. {adeu-3.2.2 → adeu-4.0.0}/tests/test_batch_validation.py +0 -0
  89. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_anchor_chrome_protection.py +0 -0
  90. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_anchor_fabrication_refusal.py +0 -0
  91. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_checkboxes.py +0 -0
  92. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_fields_ledger.py +0 -0
  93. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_fields_surfaces.py +0 -0
  94. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_gate_surfaces.py +0 -0
  95. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_gates.py +0 -0
  96. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_projection_inline.py +0 -0
  97. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_protection_state.py +0 -0
  98. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_run_control_identity.py +0 -0
  99. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_set_field.py +0 -0
  100. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_span_control_identity.py +0 -0
  101. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_widening_walls.py +0 -0
  102. {adeu-3.2.2 → adeu-4.0.0}/tests/test_changes_ledger.py +0 -0
  103. {adeu-3.2.2 → adeu-4.0.0}/tests/test_clean_view.py +0 -0
  104. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_bug_repro.py +0 -0
  105. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_encoding.py +0 -0
  106. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_features.py +0 -0
  107. {adeu-3.2.2 → adeu-4.0.0}/tests/test_comment_list.py +0 -0
  108. {adeu-3.2.2 → adeu-4.0.0}/tests/test_comment_xml_structure.py +0 -0
  109. {adeu-3.2.2 → adeu-4.0.0}/tests/test_comments.py +0 -0
  110. {adeu-3.2.2 → adeu-4.0.0}/tests/test_comments_threading.py +0 -0
  111. {adeu-3.2.2 → adeu-4.0.0}/tests/test_compact_diff_json.py +0 -0
  112. {adeu-3.2.2 → adeu-4.0.0}/tests/test_content_control_classification.py +0 -0
  113. {adeu-3.2.2 → adeu-4.0.0}/tests/test_context_trimming.py +0 -0
  114. {adeu-3.2.2 → adeu-4.0.0}/tests/test_corpus_validation.py +0 -0
  115. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cross_platform_consistency.py +0 -0
  116. {adeu-3.2.2 → adeu-4.0.0}/tests/test_cross_platform_integrity.py +0 -0
  117. {adeu-3.2.2 → adeu-4.0.0}/tests/test_diff_logic.py +0 -0
  118. {adeu-3.2.2 → adeu-4.0.0}/tests/test_diff_subalignment_and_balance.py +0 -0
  119. {adeu-3.2.2 → adeu-4.0.0}/tests/test_disk_projection_cache.py +0 -0
  120. {adeu-3.2.2 → adeu-4.0.0}/tests/test_doc_cache.py +0 -0
  121. {adeu-3.2.2 → adeu-4.0.0}/tests/test_document_settings_warnings.py +0 -0
  122. {adeu-3.2.2 → adeu-4.0.0}/tests/test_domain_semantics.py +0 -0
  123. {adeu-3.2.2 → adeu-4.0.0}/tests/test_edit_report_index.py +0 -0
  124. {adeu-3.2.2 → adeu-4.0.0}/tests/test_explicit_salvage.py +0 -0
  125. {adeu-3.2.2 → adeu-4.0.0}/tests/test_failure_envelope.py +0 -0
  126. {adeu-3.2.2 → adeu-4.0.0}/tests/test_failure_recovery_protocol.py +0 -0
  127. {adeu-3.2.2 → adeu-4.0.0}/tests/test_fastmcp3_compat.py +0 -0
  128. {adeu-3.2.2 → adeu-4.0.0}/tests/test_file_hints_and_id_discovery.py +0 -0
  129. {adeu-3.2.2 → adeu-4.0.0}/tests/test_fixtures_synth.py +0 -0
  130. {adeu-3.2.2 → adeu-4.0.0}/tests/test_fixtures_verification.py +0 -0
  131. {adeu-3.2.2 → adeu-4.0.0}/tests/test_flat_opc_to_docx.py +0 -0
  132. {adeu-3.2.2 → adeu-4.0.0}/tests/test_formatting.py +0 -0
  133. {adeu-3.2.2 → adeu-4.0.0}/tests/test_fused_json_hint.py +0 -0
  134. {adeu-3.2.2 → adeu-4.0.0}/tests/test_fuzzy_matching.py +0 -0
  135. {adeu-3.2.2 → adeu-4.0.0}/tests/test_fuzzy_whitespace_context.py +0 -0
  136. {adeu-3.2.2 → adeu-4.0.0}/tests/test_gemini_schema_compat.py +0 -0
  137. {adeu-3.2.2 → adeu-4.0.0}/tests/test_guard_message.py +0 -0
  138. {adeu-3.2.2 → adeu-4.0.0}/tests/test_header_block_guard.py +0 -0
  139. {adeu-3.2.2 → adeu-4.0.0}/tests/test_ingest_comments.py +0 -0
  140. {adeu-3.2.2 → adeu-4.0.0}/tests/test_json_unicode.py +0 -0
  141. {adeu-3.2.2 → adeu-4.0.0}/tests/test_lazy_snapshot.py +0 -0
  142. {adeu-3.2.2 → adeu-4.0.0}/tests/test_libreoffice_probe_isolation.py +0 -0
  143. {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word.py +0 -0
  144. {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_content_controls.py +0 -0
  145. {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_dispatch.py +0 -0
  146. {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_gate_agreement.py +0 -0
  147. {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_para_id_signed_int32.py +0 -0
  148. {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_structured_insertion.py +0 -0
  149. {adeu-3.2.2 → adeu-4.0.0}/tests/test_local_script_paths.py +0 -0
  150. {adeu-3.2.2 → adeu-4.0.0}/tests/test_manual_markup_mode.py +0 -0
  151. {adeu-3.2.2 → adeu-4.0.0}/tests/test_marker_boundary_insert.py +0 -0
  152. {adeu-3.2.2 → adeu-4.0.0}/tests/test_markup.py +0 -0
  153. {adeu-3.2.2 → adeu-4.0.0}/tests/test_markup_fixes.py +0 -0
  154. {adeu-3.2.2 → adeu-4.0.0}/tests/test_mcp_apply_text_revision.py +0 -0
  155. {adeu-3.2.2 → adeu-4.0.0}/tests/test_mcp_diff_output.py +0 -0
  156. {adeu-3.2.2 → adeu-4.0.0}/tests/test_mcp_reasoning_optional.py +0 -0
  157. {adeu-3.2.2 → adeu-4.0.0}/tests/test_modern_comments.py +0 -0
  158. {adeu-3.2.2 → adeu-4.0.0}/tests/test_multi_run_comment_coalescing.py +0 -0
  159. {adeu-3.2.2 → adeu-4.0.0}/tests/test_natural_diffs.py +0 -0
  160. {adeu-3.2.2 → adeu-4.0.0}/tests/test_nested_markdown.py +0 -0
  161. {adeu-3.2.2 → adeu-4.0.0}/tests/test_no_chrome.py +0 -0
  162. {adeu-3.2.2 → adeu-4.0.0}/tests/test_onestep_compare.py +0 -0
  163. {adeu-3.2.2 → adeu-4.0.0}/tests/test_ooxml_schema_compliance.py +0 -0
  164. {adeu-3.2.2 → adeu-4.0.0}/tests/test_opc_document_types.py +0 -0
  165. {adeu-3.2.2 → adeu-4.0.0}/tests/test_outline_fast_equivalence.py +0 -0
  166. {adeu-3.2.2 → adeu-4.0.0}/tests/test_page_ranges.py +0 -0
  167. {adeu-3.2.2 → adeu-4.0.0}/tests/test_pagination_scanner_equivalence.py +0 -0
  168. {adeu-3.2.2 → adeu-4.0.0}/tests/test_paragraph_split_reject_parity.py +0 -0
  169. {adeu-3.2.2 → adeu-4.0.0}/tests/test_projection_goldens.py +0 -0
  170. {adeu-3.2.2 → adeu-4.0.0}/tests/test_properties.py +0 -0
  171. {adeu-3.2.2 → adeu-4.0.0}/tests/test_property_invariants.py +0 -0
  172. {adeu-3.2.2 → adeu-4.0.0}/tests/test_pure_comment_disk.py +0 -0
  173. {adeu-3.2.2 → adeu-4.0.0}/tests/test_read_ergonomics.py +0 -0
  174. {adeu-3.2.2 → adeu-4.0.0}/tests/test_regressions.py +0 -0
  175. {adeu-3.2.2 → adeu-4.0.0}/tests/test_reject_and_nested_redline.py +0 -0
  176. {adeu-3.2.2 → adeu-4.0.0}/tests/test_report_minimal.py +0 -0
  177. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_accept_all_changes_leak.py +0 -0
  178. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_accept_all_table_row_loss.py +0 -0
  179. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_accept_data_loss.py +0 -0
  180. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_batch_5.py +0 -0
  181. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_benchmark_schema_failures.py +0 -0
  182. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_block_marker_paragraph_parity.py +0 -0
  183. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_boilerplate_structure.py +0 -0
  184. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_chg_id_coalescing.py +0 -0
  185. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comment_fragment_reject.py +0 -0
  186. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comment_reference_style.py +0 -0
  187. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comment_threading_anchoring_typography.py +0 -0
  188. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comments_cleanup.py +0 -0
  189. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comments_namespace_fix.py +0 -0
  190. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_customxml_missing_part.py +0 -0
  191. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_feedback_layer.py +0 -0
  192. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_feedback_observations.py +0 -0
  193. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_foreign_comment_range_modify.py +0 -0
  194. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_formatting_bugs.py +0 -0
  195. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_fte_2026_07_16.py +0 -0
  196. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_fte_2026_07_16_run2.py +0 -0
  197. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_fuzzy_regex_timeout.py +0 -0
  198. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_heading_bug.py +0 -0
  199. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_heading_section_insert.py +0 -0
  200. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_id_pool_renumber.py +0 -0
  201. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_indexed_edits.py +0 -0
  202. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_insertion_anchor.py +0 -0
  203. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_invisible_replies.py +0 -0
  204. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue157_tab_preservation.py +0 -0
  205. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue158_paragraph_delete_reject.py +0 -0
  206. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue159_comment_timestamp.py +0 -0
  207. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue23.py +0 -0
  208. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue_114_cross_part_revisions.py +0 -0
  209. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_list_matching.py +0 -0
  210. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_live_run_fragmentation.py +0 -0
  211. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_markdown_boundary.py +0 -0
  212. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_markdown_newline.py +0 -0
  213. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_marker_preserving_edits.py +0 -0
  214. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_multilevel_lists.py +0 -0
  215. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_multirun_deletion_ids.py +0 -0
  216. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_nested_corruption.py +0 -0
  217. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_nested_insertions.py +0 -0
  218. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_nested_table_row_cell_leakage.py +0 -0
  219. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_para_id_signed_int32.py +0 -0
  220. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_projection_parity_gaps.py +0 -0
  221. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_2026_07_18.py +0 -0
  222. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_customer_assessment_2026_07_23.py +0 -0
  223. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_issues_v2.py +0 -0
  224. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_engine.py +0 -0
  225. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_mcp.py +0 -0
  226. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_rendering.py +0 -0
  227. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_reports.py +0 -0
  228. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_issues_2026_07_20.py +0 -0
  229. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report.py +0 -0
  230. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_cli_2026_07_22.py +0 -0
  231. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v2.py +0 -0
  232. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v3.py +0 -0
  233. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v4.py +0 -0
  234. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v5.py +0 -0
  235. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v6.py +0 -0
  236. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v8.py +0 -0
  237. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v9.py +0 -0
  238. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_round3_2026_07_24.py +0 -0
  239. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_raw_ooxml_in_projection.py +0 -0
  240. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_redline_replay_wrong_document.py +0 -0
  241. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_rejected_batch_action_leak.py +0 -0
  242. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_report_boundary_failures.py +0 -0
  243. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_resolved_comment_marker_parity.py +0 -0
  244. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_round16_bugs.py +0 -0
  245. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_sdt_table_row_cell_invisibility.py +0 -0
  246. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_stranded_comment_anchor.py +0 -0
  247. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_surgical_word_diff.py +0 -0
  248. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_table_regressions.py +0 -0
  249. {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_v4_report.py +0 -0
  250. {adeu-3.2.2 → adeu-4.0.0}/tests/test_resolved_numbering_and_extract_output.py +0 -0
  251. {adeu-3.2.2 → adeu-4.0.0}/tests/test_response_budget_guard.py +0 -0
  252. {adeu-3.2.2 → adeu-4.0.0}/tests/test_roundtrip.py +0 -0
  253. {adeu-3.2.2 → adeu-4.0.0}/tests/test_run_fusion_equivalence.py +0 -0
  254. {adeu-3.2.2 → adeu-4.0.0}/tests/test_safety.py +0 -0
  255. {adeu-3.2.2 → adeu-4.0.0}/tests/test_sanitize.py +0 -0
  256. {adeu-3.2.2 → adeu-4.0.0}/tests/test_search_paging.py +0 -0
  257. {adeu-3.2.2 → adeu-4.0.0}/tests/test_search_write_engine.py +0 -0
  258. {adeu-3.2.2 → adeu-4.0.0}/tests/test_serve_daemon.py +0 -0
  259. {adeu-3.2.2 → adeu-4.0.0}/tests/test_server.py +0 -0
  260. {adeu-3.2.2 → adeu-4.0.0}/tests/test_source_encoding.py +0 -0
  261. {adeu-3.2.2 → adeu-4.0.0}/tests/test_stringified_json_search_page.py +0 -0
  262. {adeu-3.2.2 → adeu-4.0.0}/tests/test_structural_edits.py +0 -0
  263. {adeu-3.2.2 → adeu-4.0.0}/tests/test_style_cache_optimization.py +0 -0
  264. {adeu-3.2.2 → adeu-4.0.0}/tests/test_styling_and_lists.py +0 -0
  265. {adeu-3.2.2 → adeu-4.0.0}/tests/test_substitution_cluster.py +0 -0
  266. {adeu-3.2.2 → adeu-4.0.0}/tests/test_table_creation.py +0 -0
  267. {adeu-3.2.2 → adeu-4.0.0}/tests/test_tables.py +0 -0
  268. {adeu-3.2.2 → adeu-4.0.0}/tests/test_terse_errors.py +0 -0
  269. {adeu-3.2.2 → adeu-4.0.0}/tests/test_tool_description_workflows.py +0 -0
  270. {adeu-3.2.2 → adeu-4.0.0}/tests/test_twin_projection_parity.py +0 -0
  271. {adeu-3.2.2 → adeu-4.0.0}/tests/test_whole_paragraph_delete_reject.py +0 -0
  272. {adeu-3.2.2 → adeu-4.0.0}/tests/test_workflow_blocking.py +0 -0
  273. {adeu-3.2.2 → adeu-4.0.0}/tests/test_xml_debug.py +0 -0
  274. {adeu-3.2.2 → adeu-4.0.0}/tests/utils.py +0 -0
  275. {adeu-3.2.2 → adeu-4.0.0}/tests/verify_sanitized.py +0 -0
  276. {adeu-3.2.2 → adeu-4.0.0}/tests/word_com.py +0 -0
adeu-4.0.0/Dockerfile ADDED
@@ -0,0 +1,12 @@
1
+ FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy the project files
6
+ COPY . /app
7
+
8
+ # Install dependencies including the optional `mcp` extra (frozen to lockfile, no dev dependencies)
9
+ RUN uv sync --frozen --extra mcp --no-dev
10
+
11
+ # Start the MCP server using the entrypoint defined in pyproject.toml
12
+ ENTRYPOINT ["uv", "run", "adeu-server"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: adeu
3
- Version: 3.2.2
3
+ Version: 4.0.0
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
@@ -37,8 +37,6 @@ Classifier: Programming Language :: Python :: 3.12
37
37
  Classifier: Programming Language :: Python :: 3.13
38
38
  Requires-Python: >=3.12
39
39
  Requires-Dist: diff-match-patch>=20230430
40
- Requires-Dist: fastmcp[apps]<4,>=3.4.7
41
- Requires-Dist: jinja2>=3.1.6
42
40
  Requires-Dist: lxml>=5.0.0
43
41
  Requires-Dist: pydantic>=2.12
44
42
  Requires-Dist: python-docx>=1.1.0
@@ -46,6 +44,12 @@ Requires-Dist: pywin32; sys_platform == 'win32'
46
44
  Requires-Dist: rapidfuzz>=3.14.5
47
45
  Requires-Dist: regex>=2024.11.6
48
46
  Requires-Dist: structlog>=24.0.0
47
+ Provides-Extra: all
48
+ Requires-Dist: fastmcp[apps]<4,>=3.4.7; extra == 'all'
49
+ Requires-Dist: jinja2>=3.1.6; extra == 'all'
50
+ Provides-Extra: mcp
51
+ Requires-Dist: fastmcp[apps]<4,>=3.4.7; extra == 'mcp'
52
+ Requires-Dist: jinja2>=3.1.6; extra == 'mcp'
49
53
  Description-Content-Type: text/markdown
50
54
 
51
55
  # Adeu: Python Toolchain & MCP Server
@@ -183,7 +187,7 @@ The I/O contract (see `docs/cli-agent-spec.md` for the full specification):
183
187
 
184
188
  ## The Python SDK
185
189
 
186
- The SDK allows you to embed Adeu's Redline Engine directly into your own Python applications.
190
+ The SDK allows you to embed Adeu's Redline Engine directly into your own Python applications without pulling in MCP server dependencies (`pip install adeu` or `uv add adeu`).
187
191
 
188
192
  ### Applying Tracked Changes
189
193
  The engine processes a flat list of `DocumentChange` objects (`ModifyText`, `SetField`, `AcceptChange`, `RejectChange`, `ReplyComment`, `InsertTableRow`, `DeleteTableRow`).
@@ -251,13 +255,14 @@ print(result.report_text)
251
255
 
252
256
  ## The MCP Server
253
257
 
254
- The Python backend exposes Adeu's capabilities to AI agents via the Model Context Protocol (MCP), powered by FastMCP.
258
+ The Python backend exposes Adeu's capabilities to AI agents via the Model Context Protocol (MCP), powered by FastMCP (shipped in the optional `adeu[mcp]` extra so SDK and CLI installs stay slim).
255
259
 
256
260
  ### Running the Server
257
261
  You can boot the server over stdio for agent consumption:
258
262
  ```bash
259
- uvx --from adeu adeu-server
263
+ uvx --from "adeu[mcp]" adeu-server
260
264
  ```
265
+ Or install into an existing environment with `pip install "adeu[mcp]"` / `uv add "adeu[mcp]"`.
261
266
 
262
267
  ### Claude Desktop Integration
263
268
  Adeu provides an initialization command to automatically inject the MCP server into your local Claude Desktop configuration.
@@ -133,7 +133,7 @@ The I/O contract (see `docs/cli-agent-spec.md` for the full specification):
133
133
 
134
134
  ## The Python SDK
135
135
 
136
- The SDK allows you to embed Adeu's Redline Engine directly into your own Python applications.
136
+ The SDK allows you to embed Adeu's Redline Engine directly into your own Python applications without pulling in MCP server dependencies (`pip install adeu` or `uv add adeu`).
137
137
 
138
138
  ### Applying Tracked Changes
139
139
  The engine processes a flat list of `DocumentChange` objects (`ModifyText`, `SetField`, `AcceptChange`, `RejectChange`, `ReplyComment`, `InsertTableRow`, `DeleteTableRow`).
@@ -201,13 +201,14 @@ print(result.report_text)
201
201
 
202
202
  ## The MCP Server
203
203
 
204
- The Python backend exposes Adeu's capabilities to AI agents via the Model Context Protocol (MCP), powered by FastMCP.
204
+ The Python backend exposes Adeu's capabilities to AI agents via the Model Context Protocol (MCP), powered by FastMCP (shipped in the optional `adeu[mcp]` extra so SDK and CLI installs stay slim).
205
205
 
206
206
  ### Running the Server
207
207
  You can boot the server over stdio for agent consumption:
208
208
  ```bash
209
- uvx --from adeu adeu-server
209
+ uvx --from "adeu[mcp]" adeu-server
210
210
  ```
211
+ Or install into an existing environment with `pip install "adeu[mcp]"` / `uv add "adeu[mcp]"`.
211
212
 
212
213
  ### Claude Desktop Integration
213
214
  Adeu provides an initialization command to automatically inject the MCP server into your local Claude Desktop configuration.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adeu"
3
- version = "3.2.2"
3
+ version = "4.0.0"
4
4
  description = "Automated DOCX Redlining Engine"
5
5
  authors = [
6
6
  { name = "Mikko Korpela" },
@@ -24,13 +24,20 @@ dependencies = [
24
24
  "pydantic>=2.12",
25
25
  "lxml>=5.0.0",
26
26
  "diff-match-patch>=20230430",
27
- "fastmcp[apps]>=3.4.7,<4",
28
- "jinja2>=3.1.6",
29
27
  "pywin32; sys_platform == 'win32'",
30
28
  "rapidfuzz>=3.14.5",
31
29
  "regex>=2024.11.6",
32
30
  ]
33
31
 
32
+ [project.optional-dependencies]
33
+ mcp = [
34
+ "fastmcp[apps]>=3.4.7,<4",
35
+ "jinja2>=3.1.6",
36
+ ]
37
+ all = [
38
+ "adeu[mcp]",
39
+ ]
40
+
34
41
  [project.urls]
35
42
  Homepage = "https://adeu.ai"
36
43
  Repository = "https://github.com/dealfluence/adeu"
@@ -45,7 +52,11 @@ requires = ["hatchling"]
45
52
  build-backend = "hatchling.build"
46
53
 
47
54
  [dependency-groups]
55
+ # Keep fastmcp[apps] and jinja2 mirrored with [project.optional-dependencies].mcp
56
+ # (enforced by tests/test_optional_mcp.py) so plain `uv sync` installs the full suite.
48
57
  dev = [
58
+ "fastmcp[apps]>=3.4.7,<4",
59
+ "jinja2>=3.1.6",
49
60
  "pytest",
50
61
  "ruff",
51
62
  "mypy",
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/dealfluence/adeu",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.2.2",
9
+ "version": "4.0.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@adeu/mcp-server",
14
- "version": "3.2.2",
14
+ "version": "4.0.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
@@ -143,7 +143,7 @@ def handle_init(args: argparse.Namespace):
143
143
  # PyPI release, so the MCP server silently drifts away from the CLI
144
144
  # the user tested (QA 2026-07-19 F-16).
145
145
  version, _sha, _ = get_build_info()
146
- package_ref = f"adeu=={version}" if version and version not in ("unknown", "0.0.0") else "adeu"
146
+ package_ref = f"adeu[mcp]=={version}" if version and version not in ("unknown", "0.0.0") else "adeu[mcp]"
147
147
 
148
148
  mcp_servers["adeu"] = {
149
149
  "command": uvx_path, # absolute path, not bare "uvx"
@@ -1,4 +1,5 @@
1
1
  import re
2
+ from pathlib import Path
2
3
  from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
3
4
 
4
5
  import structlog
@@ -1635,3 +1636,131 @@ def create_unified_diff(
1635
1636
  b_lines = modified_text.splitlines(keepends=True)
1636
1637
  diff = difflib.unified_diff(a_lines, b_lines, fromfile=fromfile, tofile=tofile)
1637
1638
  return "".join(diff)
1639
+
1640
+
1641
+ _CM_DELIMITER_RE = re.compile(r"\{--|\{\+\+|\{>>|\{==|--\}|\+\+\}|<<\}|==\}")
1642
+
1643
+
1644
+ def _clamp_display_to_criticmarkup_blocks(
1645
+ raw_target: str, raw_new: str, prefix_len: int, suffix_len: int
1646
+ ) -> tuple[int, int]:
1647
+ """
1648
+ Backtracks trim_common_context's cut points so neither display string is
1649
+ sliced INSIDE a CriticMarkup block: trimming the common `{--A ` prefix of
1650
+ `{--A B--}` -> `{--A C--}` would emit the orphaned-delimiter payloads
1651
+ `B--}` / `C--}` (QA 2026-07-23 F15). Cuts only ever move OUTWARD (prefix
1652
+ shrinks to the block start, suffix shrinks to just past the block end), so
1653
+ the display region stays a superset of the semantic change.
1654
+ """
1655
+ span_cache = {s: [(m.start(), m.end()) for m in CRITICMARKUP_BLOCK_RE.finditer(s)] for s in (raw_target, raw_new)}
1656
+
1657
+ changed = True
1658
+ while changed:
1659
+ changed = False
1660
+ for s in (raw_target, raw_new):
1661
+ for b_start, b_end in span_cache[s]:
1662
+ if b_start < prefix_len < b_end:
1663
+ prefix_len = b_start
1664
+ changed = True
1665
+ suffix_cut = len(s) - suffix_len
1666
+ if b_start < suffix_cut < b_end:
1667
+ suffix_len = max(0, len(s) - b_end)
1668
+ changed = True
1669
+ return prefix_len, suffix_len
1670
+
1671
+
1672
+ def _escape_newlines_inside_blocks(s: str) -> str:
1673
+ """
1674
+ Keeps each CriticMarkup block on ONE diff payload line by escaping its
1675
+ internal newlines — a multi-line {>>…<<} bubble otherwise ends a `+` line
1676
+ mid-block and leaks bare closers onto continuation lines (F15).
1677
+ """
1678
+ return CRITICMARKUP_BLOCK_RE.sub(lambda m: m.group(0).replace("\n", "\\n"), s)
1679
+
1680
+
1681
+ def _balance_context_line(s: str) -> str:
1682
+ """
1683
+ Trims a display-only context window so every CriticMarkup delimiter on the
1684
+ line is paired: the fixed-width window legally slices mid-block, leaving
1685
+ an opener whose closer (or a closer whose opener) lies outside the window
1686
+ (F15). Drops the line's head through the last opener-less closer and
1687
+ truncates at the first closer-less opener.
1688
+ """
1689
+ depth = 0
1690
+ cut_start = 0
1691
+ for m in _CM_DELIMITER_RE.finditer(s):
1692
+ if m.group(0).startswith("{"):
1693
+ depth += 1
1694
+ elif depth == 0:
1695
+ cut_start = m.end()
1696
+ else:
1697
+ depth -= 1
1698
+ s = s[cut_start:]
1699
+
1700
+ stack: list[int] = []
1701
+ for m in _CM_DELIMITER_RE.finditer(s):
1702
+ if m.group(0).startswith("{"):
1703
+ stack.append(m.start())
1704
+ elif stack:
1705
+ stack.pop()
1706
+ if stack:
1707
+ s = s[: stack[0]]
1708
+ return s
1709
+
1710
+
1711
+ def format_word_patch_diff(
1712
+ original_path: str,
1713
+ modified_path: str,
1714
+ text_orig: str,
1715
+ edits: List[ModifyText],
1716
+ ) -> str:
1717
+ """Format precomputed `ModifyText` hunks into Adeu's `@@ Word Patch @@` view."""
1718
+ output = [
1719
+ f"--- {Path(original_path).name}",
1720
+ f"+++ {Path(modified_path).name}",
1721
+ "",
1722
+ ]
1723
+ context_size = 40
1724
+
1725
+ for edit in edits:
1726
+ raw_start = getattr(edit, "_match_start_index", 0) or 0
1727
+ raw_target = edit.target_text or ""
1728
+ raw_new = edit.new_text or ""
1729
+
1730
+ prefix_len, suffix_len = trim_common_context(raw_target, raw_new)
1731
+ prefix_len, suffix_len = _clamp_display_to_criticmarkup_blocks(raw_target, raw_new, prefix_len, suffix_len)
1732
+
1733
+ target_end_in_target = len(raw_target) - suffix_len
1734
+ new_end_in_new = len(raw_new) - suffix_len
1735
+
1736
+ display_target = _escape_newlines_inside_blocks(raw_target[prefix_len:target_end_in_target])
1737
+ display_new = _escape_newlines_inside_blocks(raw_new[prefix_len:new_end_in_new])
1738
+
1739
+ change_start = raw_start + prefix_len
1740
+ change_end = change_start + (target_end_in_target - prefix_len)
1741
+
1742
+ pre_start = max(0, change_start - context_size)
1743
+ pre_context = text_orig[pre_start:change_start]
1744
+ pre_context = _balance_context_line(pre_context.replace("\n", " ").replace("\r", ""))
1745
+ if pre_start > 0:
1746
+ pre_context = "..." + pre_context
1747
+
1748
+ post_end = min(len(text_orig), change_end + context_size)
1749
+ post_context = text_orig[change_end:post_end]
1750
+ post_context = _balance_context_line(post_context.replace("\n", " ").replace("\r", ""))
1751
+ if post_end < len(text_orig):
1752
+ post_context = post_context + "..."
1753
+
1754
+ output.append("@@ Word Patch @@")
1755
+ output.append(f" {pre_context}")
1756
+ if display_target:
1757
+ output.append(f"- {display_target}")
1758
+ if display_new:
1759
+ output.append(f"+ {display_new}")
1760
+ output.append(f" {post_context}")
1761
+ output.append("")
1762
+
1763
+ return "\n".join(output)
1764
+
1765
+
1766
+ _create_diff_output = format_word_patch_diff
@@ -0,0 +1 @@
1
+ {"git_sha": "cab9c6c", "build_timestamp": "20261113103944"}
@@ -7,14 +7,40 @@ import sys
7
7
  import time
8
8
  from io import BytesIO
9
9
  from pathlib import Path
10
- from typing import Annotated, Any, List, Literal, Optional, Union
10
+ from typing import TYPE_CHECKING, Annotated, Any, List, Literal, Optional, Union
11
11
 
12
- from fastmcp import Context
13
- from fastmcp.exceptions import ToolError
14
- from fastmcp.tools import ToolResult, tool
15
12
  from pydantic import BeforeValidator, Field, TypeAdapter, WithJsonSchema
16
13
 
17
- from adeu.diff import generate_edits_from_text
14
+ if TYPE_CHECKING:
15
+ from fastmcp import Context
16
+ from fastmcp.exceptions import ToolError
17
+ from fastmcp.tools import ToolResult, tool
18
+ else:
19
+ try:
20
+ from fastmcp import Context
21
+ from fastmcp.exceptions import ToolError
22
+ from fastmcp.tools import ToolResult, tool
23
+ except ModuleNotFoundError:
24
+
25
+ class Context:
26
+ pass
27
+
28
+ class ToolError(RuntimeError):
29
+ pass
30
+
31
+ class ToolResult:
32
+ def __init__(self, content: Any = None, structured_content: Any = None) -> None:
33
+ self.content = content
34
+ self.structured_content = structured_content
35
+
36
+ def tool(*_args: Any, **_kwargs: Any) -> Any:
37
+ return lambda fn: fn
38
+
39
+
40
+ from adeu.diff import (
41
+ format_word_patch_diff,
42
+ generate_edits_from_text,
43
+ )
18
44
  from adeu.ingest import extract_text_from_stream
19
45
  from adeu.mcp_components._response_builders import (
20
46
  BuilderError,
@@ -1037,137 +1063,7 @@ async def diff_docx_files(
1037
1063
  return add_timing_if_debug(start_time, res)
1038
1064
 
1039
1065
 
1040
- def _clamp_display_to_criticmarkup_blocks(
1041
- raw_target: str, raw_new: str, prefix_len: int, suffix_len: int
1042
- ) -> tuple[int, int]:
1043
- """
1044
- Backtracks trim_common_context's cut points so neither display string is
1045
- sliced INSIDE a CriticMarkup block: trimming the common `{--A ` prefix of
1046
- `{--A B--}` -> `{--A C--}` would emit the orphaned-delimiter payloads
1047
- `B--}` / `C--}` (QA 2026-07-23 F15). Cuts only ever move OUTWARD (prefix
1048
- shrinks to the block start, suffix shrinks to just past the block end), so
1049
- the display region stays a superset of the semantic change.
1050
- """
1051
- from adeu.diff import CRITICMARKUP_BLOCK_RE
1052
-
1053
- span_cache = {s: [(m.start(), m.end()) for m in CRITICMARKUP_BLOCK_RE.finditer(s)] for s in (raw_target, raw_new)}
1054
-
1055
- changed = True
1056
- while changed:
1057
- changed = False
1058
- for s in (raw_target, raw_new):
1059
- for b_start, b_end in span_cache[s]:
1060
- if b_start < prefix_len < b_end:
1061
- prefix_len = b_start
1062
- changed = True
1063
- suffix_cut = len(s) - suffix_len
1064
- if b_start < suffix_cut < b_end:
1065
- suffix_len = max(0, len(s) - b_end)
1066
- changed = True
1067
- return prefix_len, suffix_len
1068
-
1069
-
1070
- def _escape_newlines_inside_blocks(s: str) -> str:
1071
- """
1072
- Keeps each CriticMarkup block on ONE diff payload line by escaping its
1073
- internal newlines — a multi-line {>>…<<} bubble otherwise ends a `+` line
1074
- mid-block and leaks bare closers onto continuation lines (F15).
1075
- """
1076
- from adeu.diff import CRITICMARKUP_BLOCK_RE
1077
-
1078
- return CRITICMARKUP_BLOCK_RE.sub(lambda m: m.group(0).replace("\n", "\\n"), s)
1079
-
1080
-
1081
- _CM_DELIMITER_RE = re.compile(r"\{--|\{\+\+|\{>>|\{==|--\}|\+\+\}|<<\}|==\}")
1082
-
1083
-
1084
- def _balance_context_line(s: str) -> str:
1085
- """
1086
- Trims a display-only context window so every CriticMarkup delimiter on the
1087
- line is paired: the fixed-width window legally slices mid-block, leaving
1088
- an opener whose closer (or a closer whose opener) lies outside the window
1089
- (F15). Drops the line's head through the last opener-less closer and
1090
- truncates at the first closer-less opener.
1091
- """
1092
- # Head: drop through the LAST closer with no opener inside the window.
1093
- depth = 0
1094
- cut_start = 0
1095
- for m in _CM_DELIMITER_RE.finditer(s):
1096
- if m.group(0).startswith("{"):
1097
- depth += 1
1098
- elif depth == 0:
1099
- cut_start = m.end()
1100
- else:
1101
- depth -= 1
1102
- s = s[cut_start:]
1103
-
1104
- # Tail: truncate at the FIRST opener whose closer is beyond the window.
1105
- stack: list[int] = []
1106
- for m in _CM_DELIMITER_RE.finditer(s):
1107
- if m.group(0).startswith("{"):
1108
- stack.append(m.start())
1109
- elif stack:
1110
- stack.pop()
1111
- if stack:
1112
- s = s[: stack[0]]
1113
- return s
1114
-
1115
-
1116
- def _create_diff_output(original_path: str, modified_path: str, text_orig: str, edits: List[ModifyText]):
1117
- from adeu.diff import trim_common_context
1118
-
1119
- output = [
1120
- f"--- {Path(original_path).name}",
1121
- f"+++ {Path(modified_path).name}",
1122
- "",
1123
- ]
1124
- CONTEXT_SIZE = 40
1125
-
1126
- for edit in edits:
1127
- raw_start = getattr(edit, "_match_start_index", 0) or 0
1128
- raw_target = edit.target_text or ""
1129
- raw_new = edit.new_text or ""
1130
-
1131
- # Compute the SEMANTIC change region by stripping common context that
1132
- # `generate_edits_from_text` baked into target_text/new_text (anchor for
1133
- # synthetic insertions, common prefix/suffix from coalesced edits).
1134
- prefix_len, suffix_len = trim_common_context(raw_target, raw_new)
1135
- # Never cut a display string inside a CriticMarkup block (F15).
1136
- prefix_len, suffix_len = _clamp_display_to_criticmarkup_blocks(raw_target, raw_new, prefix_len, suffix_len)
1137
-
1138
- target_end_in_target = len(raw_target) - suffix_len
1139
- new_end_in_new = len(raw_new) - suffix_len
1140
-
1141
- display_target = _escape_newlines_inside_blocks(raw_target[prefix_len:target_end_in_target])
1142
- display_new = _escape_newlines_inside_blocks(raw_new[prefix_len:new_end_in_new])
1143
-
1144
- # Shift the anchor point in the original text by the stripped prefix.
1145
- change_start = raw_start + prefix_len
1146
- change_end = change_start + (target_end_in_target - prefix_len)
1147
-
1148
- # Compute context windows around the SEMANTIC change region.
1149
- pre_start = max(0, change_start - CONTEXT_SIZE)
1150
- pre_context = text_orig[pre_start:change_start]
1151
- pre_context = _balance_context_line(pre_context.replace("\n", " ").replace("\r", ""))
1152
- if pre_start > 0:
1153
- pre_context = "..." + pre_context
1154
-
1155
- post_end = min(len(text_orig), change_end + CONTEXT_SIZE)
1156
- post_context = text_orig[change_end:post_end]
1157
- post_context = _balance_context_line(post_context.replace("\n", " ").replace("\r", ""))
1158
- if post_end < len(text_orig):
1159
- post_context = post_context + "..."
1160
-
1161
- output.append("@@ Word Patch @@")
1162
- output.append(f" {pre_context}")
1163
- if display_target:
1164
- output.append(f"- {display_target}")
1165
- if display_new:
1166
- output.append(f"+ {display_new}")
1167
- output.append(f" {post_context}")
1168
- output.append("")
1169
-
1170
- return "\n".join(output)
1066
+ _create_diff_output = format_word_patch_diff
1171
1067
 
1172
1068
 
1173
1069
  @tool(
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import io
2
4
  import os
3
5
  import re
@@ -6,9 +8,6 @@ from pathlib import Path
6
8
  from typing import TYPE_CHECKING, Any, List, Optional, Tuple
7
9
 
8
10
  import structlog
9
- from fastmcp import Context
10
- from fastmcp.exceptions import ToolError
11
- from fastmcp.tools import ToolResult
12
11
 
13
12
  from adeu import RedlineEngine
14
13
  from adeu.mcp_components._response_builders import (
@@ -27,7 +26,8 @@ from adeu.utils.text import batch_details_header
27
26
  logger = structlog.get_logger(__name__)
28
27
 
29
28
  if TYPE_CHECKING:
30
- pass
29
+ from fastmcp import Context
30
+ from fastmcp.tools import ToolResult
31
31
 
32
32
 
33
33
  def _build_mock_docx_stream(word_open_xml: str) -> io.BytesIO:
@@ -344,6 +344,9 @@ if sys.platform == "win32":
344
344
  match_offset: int = 0,
345
345
  full_paragraph: bool = False,
346
346
  ) -> ToolResult:
347
+ from fastmcp.exceptions import ToolError
348
+ from fastmcp.tools import ToolResult
349
+
347
350
  await ctx.info(
348
351
  f"Extracting live Word document via WordOpenXML "
349
352
  f"(clean_view={clean_view}, path={file_path}, mode={mode}, page={page}, "
@@ -1269,6 +1272,8 @@ if sys.platform == "win32":
1269
1272
  file_path: Optional[str] = None,
1270
1273
  gate_overrides: Optional[dict] = None,
1271
1274
  ) -> str:
1275
+ from fastmcp.exceptions import ToolError
1276
+
1272
1277
  if not changes:
1273
1278
  return "No changes provided."
1274
1279
 
@@ -1298,6 +1303,8 @@ if sys.platform == "win32":
1298
1303
  raise ToolError(str(e)) from e
1299
1304
 
1300
1305
  async def open_word_document_impl(ctx: Context, file_path: str, visible: bool = True) -> str:
1306
+ from fastmcp.exceptions import ToolError
1307
+
1301
1308
  await ctx.info(f"Opening {file_path} in Word...")
1302
1309
  pythoncom.CoInitialize()
1303
1310
  try:
@@ -1321,6 +1328,8 @@ if sys.platform == "win32":
1321
1328
  async def save_active_word_document_impl(
1322
1329
  ctx: Context, output_path: Optional[str] = None, close: bool = False
1323
1330
  ) -> str:
1331
+ from fastmcp.exceptions import ToolError
1332
+
1324
1333
  await ctx.info("Saving active Word document...")
1325
1334
  pythoncom.CoInitialize()
1326
1335
  try: