adeu 3.2.1__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.1 → adeu-3.2.2}/PKG-INFO +1 -1
- {adeu-3.2.1 → adeu-3.2.2}/pyproject.toml +1 -1
- {adeu-3.2.1 → adeu-3.2.2}/server.json +2 -2
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/cli.py +50 -10
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/compare.py +14 -4
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/disk_cache.py +5 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/ingest.py +47 -2
- adeu-3.2.2/src/adeu/mcp_components/build_info.json +1 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/tools/document.py +10 -2
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/outline.py +48 -28
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/redline/engine.py +59 -7
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/redline/mapper.py +144 -36
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/sanitize/core.py +3 -3
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/text_revision.py +40 -1
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/docx.py +441 -80
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_2026_07_18.py +1 -1
- {adeu-3.2.1 → 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.1 → adeu-3.2.2}/uv.lock +1 -1
- adeu-3.2.1/src/adeu/mcp_components/build_info.json +0 -1
- {adeu-3.2.1 → adeu-3.2.2}/.gitignore +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/Dockerfile +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/LICENSE +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/README.md +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/abstract_xml.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/debug_rels.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/debug_structure.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/debug_styles.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/debug_xml.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/generate_goldens.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/golden_projection.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/inspect_test_docx.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/login_staging.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/reproduce_issue.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/test_numbered_list.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/scripts/verify_staging_polling.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/__init__.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/assets/adeu.svg +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/assets/logo.png +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/assets/marked.min.js +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/diff.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/domain.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/fields.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/markup.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/__init__.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/_response_builders.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/doc_cache.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/resources/markdown_ui.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/shared.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/tools/live_word.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/tools/live_word_ops.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/mcp_components/tools/sanitize.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/models.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/pagination.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/payloads.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/redline/comments.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/redline/gates.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/sanitize/__init__.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/sanitize/report.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/sanitize/transforms.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/serve.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/server.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/templates/markdown_ui.html +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/console.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/content_controls.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/field_write.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/long_hex_number.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/opc.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/protection.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/safe_regex.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/schema_order.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/text.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/src/adeu/utils/xml_debug.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/__init__.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/cc_fixture.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/conftest.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/docx_fixtures.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/fixtures_synth.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/golden_manifest.txt +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/repro_golden.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/sdt_fixtures.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_atomic_batch_pipeline.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_author_impersonation_gate.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_batch_ergonomics_and_receipts.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_batch_reliability.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_batch_validation.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_anchor_chrome_protection.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_anchor_fabrication_refusal.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_checkboxes.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_fields_ledger.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_fields_surfaces.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_gate_surfaces.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_gates.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_projection_inline.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_protection_state.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_run_control_identity.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_set_field.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_span_control_identity.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cc_widening_walls.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_changes_ledger.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_clean_view.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cli_bug_repro.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cli_encoding.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cli_features.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cli_init.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_comment_list.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_comment_xml_structure.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_comments.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_comments_threading.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_compact_diff_json.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_content_control_classification.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_context_trimming.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_corpus_validation.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cross_platform_consistency.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_cross_platform_integrity.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_diff_logic.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_diff_subalignment_and_balance.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_disk_projection_cache.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_doc_cache.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_document_settings_warnings.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_domain_semantics.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_edit_report_index.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_explicit_salvage.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_failure_envelope.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_failure_recovery_protocol.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_fastmcp3_compat.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_file_hints_and_id_discovery.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_fixtures_synth.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_fixtures_verification.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_flat_opc_to_docx.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_formatting.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_fused_json_hint.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_fuzzy_matching.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_fuzzy_whitespace_context.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_gemini_schema_compat.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_guard_message.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_header_block_guard.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_ingest_comments.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_json_unicode.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_lazy_snapshot.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_libreoffice_probe_isolation.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_live_word.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_live_word_content_controls.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_live_word_dispatch.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_live_word_gate_agreement.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_live_word_para_id_signed_int32.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_live_word_structured_insertion.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_local_script_paths.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_manual_markup_mode.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_marker_boundary_insert.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_markup.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_markup_fixes.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_mcp_apply_text_revision.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_mcp_diff_output.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_mcp_reasoning_optional.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_modern_comments.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_multi_run_comment_coalescing.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_natural_diffs.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_nested_markdown.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_no_chrome.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_onestep_compare.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_ooxml_schema_compliance.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_opc_document_types.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_outline_fast_equivalence.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_page_ranges.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_pagination_scanner_equivalence.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_paragraph_split_reject_parity.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_projection_goldens.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_properties.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_property_invariants.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_pure_comment_disk.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_read_ergonomics.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_regressions.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_reject_and_nested_redline.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_report_minimal.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_accept_all_changes_leak.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_accept_all_table_row_loss.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_accept_data_loss.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_batch_5.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_benchmark_schema_failures.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_block_marker_paragraph_parity.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_boilerplate_structure.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_chg_id_coalescing.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_comment_fragment_reject.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_comment_reference_style.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_comment_threading_anchoring_typography.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_comments_cleanup.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_comments_namespace_fix.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_customxml_missing_part.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_feedback_layer.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_feedback_observations.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_foreign_comment_range_modify.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_formatting_bugs.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_fte_2026_07_16.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_fte_2026_07_16_run2.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_fuzzy_regex_timeout.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_heading_bug.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_heading_section_insert.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_id_pool_renumber.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_indexed_edits.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_insertion_anchor.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_invisible_replies.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_issue157_tab_preservation.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_issue158_paragraph_delete_reject.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_issue159_comment_timestamp.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_issue23.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_issue_114_cross_part_revisions.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_list_matching.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_live_run_fragmentation.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_markdown_boundary.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_markdown_newline.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_marker_preserving_edits.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_multilevel_lists.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_multirun_deletion_ids.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_nested_corruption.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_nested_insertions.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_nested_table_row_cell_leakage.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_para_id_signed_int32.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_projection_parity_gaps.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_customer_assessment_2026_07_23.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_issues_v2.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_engine.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_mcp.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_rendering.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_mcp_2026_07_23_reports.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_mcp_issues_2026_07_20.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_cli_2026_07_22.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_v2.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_v3.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_v4.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_v6.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_v7.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_v8.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_report_v9.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_qa_round3_2026_07_24.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_raw_ooxml_in_projection.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_redline_replay_wrong_document.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_rejected_batch_action_leak.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_report_boundary_failures.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_resolved_comment_marker_parity.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_round16_bugs.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_sdt_table_row_cell_invisibility.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_stranded_comment_anchor.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_surgical_word_diff.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_table_regressions.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_repro_v4_report.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_response_budget_guard.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_roundtrip.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_run_fusion_equivalence.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_safety.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_sanitize.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_search_paging.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_search_write_engine.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_serve_daemon.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_server.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_source_encoding.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_stringified_json_search_page.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_structural_edits.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_style_cache_optimization.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_styling_and_lists.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_substitution_cluster.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_table_creation.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_tables.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_terse_errors.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_tool_description_workflows.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_twin_projection_parity.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_whole_paragraph_delete_reject.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_workflow_blocking.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/test_xml_debug.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/utils.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/verify_sanitized.py +0 -0
- {adeu-3.2.1 → adeu-3.2.2}/tests/word_com.py +0 -0
|
@@ -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
|
|
@@ -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.
|
|
@@ -16,6 +16,8 @@ from adeu.utils.docx import (
|
|
|
16
16
|
QN_W_FOOTNOTEREFERENCE,
|
|
17
17
|
QN_W_P,
|
|
18
18
|
DocxEvent,
|
|
19
|
+
NumberingTracker,
|
|
20
|
+
_get_style_cache,
|
|
19
21
|
get_paragraph_prefix,
|
|
20
22
|
iter_block_items,
|
|
21
23
|
iter_document_parts,
|
|
@@ -50,6 +52,7 @@ class _BlockRecord:
|
|
|
50
52
|
is_table: bool
|
|
51
53
|
start_offset: int # offset in projected body where this block starts
|
|
52
54
|
projected_length: int # length of the projected text for this block
|
|
55
|
+
prefix: str = ""
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
# ---------------------------------------------------------------------------
|
|
@@ -145,7 +148,7 @@ def extract_outline(
|
|
|
145
148
|
paragraph = rec.item
|
|
146
149
|
|
|
147
150
|
level = _heading_level(paragraph)
|
|
148
|
-
text = _heading_text(paragraph, comments_map)
|
|
151
|
+
text = _heading_text(paragraph, comments_map, prefix=rec.prefix)
|
|
149
152
|
style = _determine_heading_style(paragraph)
|
|
150
153
|
|
|
151
154
|
# Owned range: blocks strictly between this heading and the next equal-or-
|
|
@@ -476,10 +479,19 @@ def _walk_doc_body(doc: DocumentObject, comments_map: dict) -> List[_BlockRecord
|
|
|
476
479
|
records: List[_BlockRecord] = []
|
|
477
480
|
cursor = body_start_offset
|
|
478
481
|
is_first_block = True
|
|
482
|
+
body_part_ref = getattr(body_part, "part", body_part)
|
|
483
|
+
body_style_cache, body_default_pstyle = _get_style_cache(body_part_ref)
|
|
484
|
+
body_tracker = NumberingTracker(body_part_ref, style_cache=body_style_cache)
|
|
479
485
|
|
|
480
486
|
for item in iter_block_items(body_part):
|
|
481
487
|
if isinstance(item, Paragraph):
|
|
482
|
-
prefix = get_paragraph_prefix(
|
|
488
|
+
prefix = get_paragraph_prefix(
|
|
489
|
+
item,
|
|
490
|
+
style_cache=body_style_cache,
|
|
491
|
+
default_pstyle=body_default_pstyle,
|
|
492
|
+
part=body_part_ref,
|
|
493
|
+
numbering_tracker=body_tracker,
|
|
494
|
+
)
|
|
483
495
|
p_text = build_paragraph_text(item, comments_map, clean_view=False)
|
|
484
496
|
block_len = len(prefix + p_text)
|
|
485
497
|
|
|
@@ -493,13 +505,15 @@ def _walk_doc_body(doc: DocumentObject, comments_map: dict) -> List[_BlockRecord
|
|
|
493
505
|
is_table=False,
|
|
494
506
|
start_offset=cursor,
|
|
495
507
|
projected_length=block_len,
|
|
508
|
+
prefix=prefix,
|
|
496
509
|
)
|
|
497
510
|
)
|
|
498
511
|
cursor += block_len
|
|
499
512
|
is_first_block = False
|
|
500
513
|
|
|
501
514
|
elif isinstance(item, Table):
|
|
502
|
-
|
|
515
|
+
snap = body_tracker.snapshot()
|
|
516
|
+
table_text = extract_table(item, comments_map, clean_view=False, numbering_tracker=body_tracker)
|
|
503
517
|
block_len = len(table_text) if table_text else 0
|
|
504
518
|
|
|
505
519
|
if not is_first_block:
|
|
@@ -515,9 +529,12 @@ def _walk_doc_body(doc: DocumentObject, comments_map: dict) -> List[_BlockRecord
|
|
|
515
529
|
projected_length=block_len,
|
|
516
530
|
)
|
|
517
531
|
)
|
|
518
|
-
# Record inner blocks for heading detection —
|
|
519
|
-
#
|
|
520
|
-
|
|
532
|
+
# Record inner blocks for heading detection — rewind a copy of
|
|
533
|
+
# body_tracker so inner heading prefixes match extract_table's pass.
|
|
534
|
+
after_table = body_tracker.snapshot()
|
|
535
|
+
body_tracker.restore(snap)
|
|
536
|
+
_record_table_inner_blocks_lite(item, table_start, records, comments_map, body_tracker)
|
|
537
|
+
body_tracker.restore(after_table)
|
|
521
538
|
cursor += block_len
|
|
522
539
|
is_first_block = False
|
|
523
540
|
|
|
@@ -678,17 +695,12 @@ def _record_table_inner_blocks_lite(
|
|
|
678
695
|
inherited_offset: int,
|
|
679
696
|
records: List[_BlockRecord],
|
|
680
697
|
comments_map: dict,
|
|
698
|
+
numbering_tracker: Optional[NumberingTracker] = None,
|
|
681
699
|
) -> None:
|
|
682
700
|
"""
|
|
683
701
|
PERF-optimized version of _record_table_inner_blocks: records inner
|
|
684
702
|
paragraphs/tables for heading detection without paying the full projection
|
|
685
703
|
cost for non-headings.
|
|
686
|
-
|
|
687
|
-
Headings inside tables get TRUE offsets computed via _compute_inner_block_offset
|
|
688
|
-
(so outline.page resolves correctly when a table spans page boundaries).
|
|
689
|
-
Non-heading inner blocks keep projected_length=0 and inherit the table's
|
|
690
|
-
start_offset — they are never used to resolve a page, only to scan for
|
|
691
|
-
nested headings/tables.
|
|
692
704
|
"""
|
|
693
705
|
seen_cells: set = set()
|
|
694
706
|
for row in iter_table_rows(table):
|
|
@@ -700,10 +712,14 @@ def _record_table_inner_blocks_lite(
|
|
|
700
712
|
|
|
701
713
|
for inner_item in iter_block_items(cell):
|
|
702
714
|
if isinstance(inner_item, Paragraph):
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
715
|
+
prefix = (
|
|
716
|
+
get_paragraph_prefix(inner_item, numbering_tracker=numbering_tracker)
|
|
717
|
+
if numbering_tracker is not None
|
|
718
|
+
else ""
|
|
719
|
+
)
|
|
706
720
|
if _is_heading(inner_item):
|
|
721
|
+
if not prefix:
|
|
722
|
+
prefix = get_paragraph_prefix(inner_item)
|
|
707
723
|
true_offset = _compute_inner_block_offset(table, inner_item, inherited_offset, comments_map)
|
|
708
724
|
else:
|
|
709
725
|
true_offset = inherited_offset
|
|
@@ -715,6 +731,7 @@ def _record_table_inner_blocks_lite(
|
|
|
715
731
|
is_table=False,
|
|
716
732
|
start_offset=true_offset,
|
|
717
733
|
projected_length=0,
|
|
734
|
+
prefix=prefix,
|
|
718
735
|
)
|
|
719
736
|
)
|
|
720
737
|
elif isinstance(inner_item, Table):
|
|
@@ -727,20 +744,22 @@ def _record_table_inner_blocks_lite(
|
|
|
727
744
|
projected_length=0,
|
|
728
745
|
)
|
|
729
746
|
)
|
|
730
|
-
_record_table_inner_blocks_lite(
|
|
747
|
+
_record_table_inner_blocks_lite(
|
|
748
|
+
inner_item, inherited_offset, records, comments_map, numbering_tracker
|
|
749
|
+
)
|
|
731
750
|
|
|
732
751
|
|
|
733
|
-
def _project_part(part, comments_map: dict) -> str:
|
|
752
|
+
def _project_part(part, comments_map: dict, numbering_tracker: Optional[NumberingTracker] = None) -> str:
|
|
734
753
|
"""
|
|
735
754
|
Renders a non-body document part (header/footer/notes) to measure its
|
|
736
|
-
projected length. Mirrors ingest._extract_blocks
|
|
737
|
-
don't reach into ingest's private helpers.
|
|
738
|
-
|
|
739
|
-
Headers and footers iterate as Paragraphs/Tables. NotesPart is iterated
|
|
740
|
-
via FootnoteItem children.
|
|
755
|
+
projected length. Mirrors ingest._extract_blocks.
|
|
741
756
|
"""
|
|
742
757
|
blocks: List[str] = []
|
|
743
758
|
c_type = type(part).__name__
|
|
759
|
+
part_ref = getattr(part, "part", part)
|
|
760
|
+
if numbering_tracker is None:
|
|
761
|
+
style_cache, _ = _get_style_cache(part_ref)
|
|
762
|
+
numbering_tracker = NumberingTracker(part_ref, style_cache=style_cache)
|
|
744
763
|
|
|
745
764
|
if c_type == "NotesPart":
|
|
746
765
|
header = "## Footnotes" if part.note_type == "fn" else "## Endnotes"
|
|
@@ -751,18 +770,18 @@ def _project_part(part, comments_map: dict) -> str:
|
|
|
751
770
|
i_type = type(item).__name__
|
|
752
771
|
|
|
753
772
|
if i_type == "FootnoteItem":
|
|
754
|
-
fn_text = _project_part(item, comments_map)
|
|
773
|
+
fn_text = _project_part(item, comments_map, numbering_tracker=numbering_tracker)
|
|
755
774
|
if fn_text:
|
|
756
775
|
blocks.append(fn_text)
|
|
757
776
|
elif isinstance(item, Paragraph):
|
|
758
|
-
prefix = get_paragraph_prefix(item)
|
|
777
|
+
prefix = get_paragraph_prefix(item, part=part_ref, numbering_tracker=numbering_tracker)
|
|
759
778
|
if is_first_para and c_type == "FootnoteItem":
|
|
760
779
|
prefix = f"[^{part.note_type}-{part.id}]: " + prefix
|
|
761
780
|
p_text = build_paragraph_text(item, comments_map, clean_view=False)
|
|
762
781
|
blocks.append(prefix + p_text)
|
|
763
782
|
is_first_para = False
|
|
764
783
|
elif isinstance(item, Table):
|
|
765
|
-
table_text = extract_table(item, comments_map, clean_view=False)
|
|
784
|
+
table_text = extract_table(item, comments_map, clean_view=False, numbering_tracker=numbering_tracker)
|
|
766
785
|
if table_text:
|
|
767
786
|
blocks.append(table_text)
|
|
768
787
|
is_first_para = False
|
|
@@ -907,12 +926,12 @@ def _heading_level(paragraph: Paragraph) -> int:
|
|
|
907
926
|
# ---------------------------------------------------------------------------
|
|
908
927
|
|
|
909
928
|
|
|
910
|
-
def _heading_text(paragraph: Paragraph, comments_map: dict) -> str:
|
|
929
|
+
def _heading_text(paragraph: Paragraph, comments_map: dict, prefix: str = "") -> str:
|
|
911
930
|
"""
|
|
912
931
|
Returns the heading text as the user would see it:
|
|
913
932
|
- same projection as build_paragraph_text (so tracked changes / formatting
|
|
914
933
|
look the same as in the body)
|
|
915
|
-
- heading prefix removed
|
|
934
|
+
- heading '#' prefix removed (while preserving any resolved heading clause number)
|
|
916
935
|
- CriticMarkup tags stripped (insertions kept as plain text, deletions
|
|
917
936
|
and meta-blocks dropped)
|
|
918
937
|
- internal newlines collapsed to a single space BEFORE marker stripping —
|
|
@@ -927,7 +946,8 @@ def _heading_text(paragraph: Paragraph, comments_map: dict) -> str:
|
|
|
927
946
|
(QA 2026-07-23 F13b).
|
|
928
947
|
"""
|
|
929
948
|
p_text = build_paragraph_text(paragraph, comments_map, clean_view=False)
|
|
930
|
-
|
|
949
|
+
heading_num_label = re.sub(r"^#+\s+", "", prefix) if prefix else ""
|
|
950
|
+
cleaned = _strip_critic_markup(heading_num_label + p_text)
|
|
931
951
|
cleaned = re.sub(r"\s*\n\s*", " ", cleaned)
|
|
932
952
|
cleaned = _strip_inline_formatting(cleaned)
|
|
933
953
|
return _truncate_outline_text(cleaned.strip())
|