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.
- adeu-4.0.0/Dockerfile +12 -0
- {adeu-3.2.2 → adeu-4.0.0}/PKG-INFO +11 -6
- {adeu-3.2.2 → adeu-4.0.0}/README.md +4 -3
- {adeu-3.2.2 → adeu-4.0.0}/pyproject.toml +14 -3
- {adeu-3.2.2 → adeu-4.0.0}/server.json +2 -2
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/cli.py +1 -1
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/diff.py +129 -0
- adeu-4.0.0/src/adeu/mcp_components/build_info.json +1 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/document.py +32 -136
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/live_word.py +13 -4
- adeu-4.0.0/src/adeu/server.py +348 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_init.py +1 -1
- adeu-4.0.0/tests/test_optional_mcp.py +238 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v7.py +1 -1
- {adeu-3.2.2 → adeu-4.0.0}/uv.lock +19 -5
- adeu-3.2.2/Dockerfile +0 -12
- adeu-3.2.2/src/adeu/mcp_components/build_info.json +0 -1
- adeu-3.2.2/src/adeu/server.py +0 -138
- {adeu-3.2.2 → adeu-4.0.0}/.gitignore +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/LICENSE +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/abstract_xml.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_rels.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_structure.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_styles.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/debug_xml.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/generate_goldens.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/golden_projection.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/inspect_test_docx.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/login_staging.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/reproduce_issue.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/test_numbered_list.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/scripts/verify_staging_polling.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/__init__.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/assets/adeu.svg +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/assets/logo.png +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/assets/marked.min.js +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/compare.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/disk_cache.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/domain.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/fields.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/ingest.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/markup.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/__init__.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/_response_builders.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/doc_cache.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/resources/markdown_ui.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/shared.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/live_word_ops.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/mcp_components/tools/sanitize.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/models.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/outline.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/pagination.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/payloads.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/comments.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/engine.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/gates.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/redline/mapper.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/__init__.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/core.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/report.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/sanitize/transforms.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/serve.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/templates/markdown_ui.html +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/text_revision.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/console.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/content_controls.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/docx.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/field_write.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/long_hex_number.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/opc.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/protection.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/safe_regex.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/schema_order.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/text.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/src/adeu/utils/xml_debug.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/__init__.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/cc_fixture.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/conftest.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/docx_fixtures.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/fixtures_synth.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/golden_manifest.txt +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/repro_golden.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/sdt_fixtures.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_atomic_batch_pipeline.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_author_impersonation_gate.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_batch_ergonomics_and_receipts.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_batch_reliability.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_batch_validation.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_anchor_chrome_protection.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_anchor_fabrication_refusal.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_checkboxes.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_fields_ledger.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_fields_surfaces.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_gate_surfaces.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_gates.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_projection_inline.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_protection_state.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_run_control_identity.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_set_field.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_span_control_identity.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cc_widening_walls.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_changes_ledger.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_clean_view.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_bug_repro.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_encoding.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cli_features.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_comment_list.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_comment_xml_structure.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_comments.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_comments_threading.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_compact_diff_json.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_content_control_classification.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_context_trimming.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_corpus_validation.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cross_platform_consistency.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_cross_platform_integrity.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_diff_logic.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_diff_subalignment_and_balance.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_disk_projection_cache.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_doc_cache.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_document_settings_warnings.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_domain_semantics.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_edit_report_index.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_explicit_salvage.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_failure_envelope.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_failure_recovery_protocol.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_fastmcp3_compat.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_file_hints_and_id_discovery.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_fixtures_synth.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_fixtures_verification.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_flat_opc_to_docx.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_formatting.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_fused_json_hint.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_fuzzy_matching.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_fuzzy_whitespace_context.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_gemini_schema_compat.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_guard_message.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_header_block_guard.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_ingest_comments.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_json_unicode.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_lazy_snapshot.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_libreoffice_probe_isolation.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_content_controls.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_dispatch.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_gate_agreement.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_para_id_signed_int32.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_live_word_structured_insertion.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_local_script_paths.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_manual_markup_mode.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_marker_boundary_insert.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_markup.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_markup_fixes.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_mcp_apply_text_revision.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_mcp_diff_output.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_mcp_reasoning_optional.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_modern_comments.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_multi_run_comment_coalescing.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_natural_diffs.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_nested_markdown.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_no_chrome.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_onestep_compare.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_ooxml_schema_compliance.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_opc_document_types.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_outline_fast_equivalence.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_page_ranges.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_pagination_scanner_equivalence.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_paragraph_split_reject_parity.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_projection_goldens.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_properties.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_property_invariants.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_pure_comment_disk.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_read_ergonomics.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_regressions.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_reject_and_nested_redline.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_report_minimal.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_accept_all_changes_leak.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_accept_all_table_row_loss.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_accept_data_loss.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_batch_5.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_benchmark_schema_failures.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_block_marker_paragraph_parity.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_boilerplate_structure.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_chg_id_coalescing.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comment_fragment_reject.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comment_reference_style.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comment_threading_anchoring_typography.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comments_cleanup.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_comments_namespace_fix.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_customxml_missing_part.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_feedback_layer.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_feedback_observations.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_foreign_comment_range_modify.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_formatting_bugs.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_fte_2026_07_16.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_fte_2026_07_16_run2.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_fuzzy_regex_timeout.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_heading_bug.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_heading_section_insert.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_id_pool_renumber.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_indexed_edits.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_insertion_anchor.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_invisible_replies.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue157_tab_preservation.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue158_paragraph_delete_reject.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue159_comment_timestamp.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue23.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_issue_114_cross_part_revisions.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_list_matching.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_live_run_fragmentation.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_markdown_boundary.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_markdown_newline.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_marker_preserving_edits.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_multilevel_lists.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_multirun_deletion_ids.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_nested_corruption.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_nested_insertions.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_nested_table_row_cell_leakage.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_para_id_signed_int32.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_projection_parity_gaps.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_2026_07_18.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_customer_assessment_2026_07_23.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_issues_v2.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_engine.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_mcp.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_rendering.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_2026_07_23_reports.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_mcp_issues_2026_07_20.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_cli_2026_07_22.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v2.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v3.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v4.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v5.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v6.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v8.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_report_v9.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_qa_round3_2026_07_24.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_raw_ooxml_in_projection.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_redline_replay_wrong_document.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_rejected_batch_action_leak.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_report_boundary_failures.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_resolved_comment_marker_parity.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_round16_bugs.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_sdt_table_row_cell_invisibility.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_stranded_comment_anchor.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_surgical_word_diff.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_table_regressions.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_repro_v4_report.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_resolved_numbering_and_extract_output.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_response_budget_guard.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_roundtrip.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_run_fusion_equivalence.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_safety.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_sanitize.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_search_paging.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_search_write_engine.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_serve_daemon.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_server.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_source_encoding.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_stringified_json_search_page.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_structural_edits.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_style_cache_optimization.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_styling_and_lists.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_substitution_cluster.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_table_creation.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_tables.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_terse_errors.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_tool_description_workflows.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_twin_projection_parity.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_whole_paragraph_delete_reject.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_workflow_blocking.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/test_xml_debug.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/utils.py +0 -0
- {adeu-3.2.2 → adeu-4.0.0}/tests/verify_sanitized.py +0 -0
- {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
|
+
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
|
+
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": "
|
|
9
|
+
"version": "4.0.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@adeu/mcp-server",
|
|
14
|
-
"version": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|