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