python-hwpx 5.7.0__tar.gz → 5.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/NOTICE +8 -0
- {python_hwpx-5.7.0/src/python_hwpx.egg-info → python_hwpx-5.8.0}/PKG-INFO +9 -4
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/README.md +9 -4
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/pyproject.toml +1 -1
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/capabilities.py +151 -2
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/support-matrix.md +3 -2
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/layout/report.py +11 -2
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/patch.py +22 -1
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/report.py +34 -2
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/save_pipeline.py +21 -4
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/table_patch.py +1 -1
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/idempotence.py +7 -5
- {python_hwpx-5.7.0 → python_hwpx-5.8.0/src/python_hwpx.egg-info}/PKG-INFO +9 -4
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_capabilities_surface.py +55 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/LICENSE +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/setup.cfg +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/mutation-semantics.md +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/recipes-traversal.md +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/document.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/authoring.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/mathml.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/render.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/tokens.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/errors.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/experimental.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/package.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_execute.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_model.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_schema.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_validate.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/SOURCES.txt +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_add_control.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_adoption_metrics_snapshot.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_authoring_defaults.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_chart_authoring.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_check_box_authoring.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_contract_docs_sync.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_viewer.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_documentation_code_fences.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_edit_plan.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_equation_authoring.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_equation_converter.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_equation_render.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_error_messages.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_field_authoring.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_golden_api.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_ingest.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_llms_txt.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_mutation_report.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_new_features.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_note_authoring_contract.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_product_boundary.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_release_workflow_safety.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_stable_surface.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_version_metadata.py +0 -0
|
@@ -58,6 +58,14 @@ neolord0/hwpxlib (https://github.com/neolord0/hwpxlib), licensed Apache-2.0.
|
|
|
58
58
|
Only sample DATA files are vendored; no hwpxlib source code or structure is
|
|
59
59
|
copied (clean-room). Pinned ref is recorded in manifest.json.
|
|
60
60
|
|
|
61
|
+
## Hancom file-format specification attribution
|
|
62
|
+
|
|
63
|
+
Parts of this project were developed with reference to the publicly
|
|
64
|
+
released Hancom file-format documentation (HWP 5.0 / HWPML) and the
|
|
65
|
+
public OWPML schema. As required by that documentation:
|
|
66
|
+
|
|
67
|
+
“본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.”
|
|
68
|
+
|
|
61
69
|
────────────────────────────────────────
|
|
62
70
|
DISCLAIMER — HWPX Format
|
|
63
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -158,10 +158,15 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
158
158
|
|
|
159
159
|
## 실측으로 말합니다
|
|
160
160
|
|
|
161
|
-
만든 파일이 실제 한컴오피스에서
|
|
162
|
-
|
|
161
|
+
만든 파일이 실제 한컴오피스에서 열리는지 재서 그대로 공개합니다. 측정마다
|
|
162
|
+
스택과 날짜를 병기합니다:
|
|
163
163
|
|
|
164
|
-
- **한컴 열림
|
|
164
|
+
- **한컴 열림 120/120 · 렌더 검증 120/120** — 현행 스택(5.7.0 · 실한컴
|
|
165
|
+
12.0.0.3288 · 2026-08-03)의 축소 측정: 기준 스트라텀 + 신규 저작 표면
|
|
166
|
+
6종(각주·누름틀·수식·차트·체크박스·편집 계획). 영수증은 행마다 bucket이
|
|
167
|
+
있어 `jq` 한 줄로 재현됩니다
|
|
168
|
+
- **한컴 열림 476/476** — 전수 동결 코퍼스(N=497) 측정
|
|
169
|
+
(3.4.1 · 실한컴 12.0.0.3288 · 2026-07-19)
|
|
165
170
|
- **미수정 영역 바이트 보존 497/497** · 개인정보 유출 0
|
|
166
171
|
- **렌더 검증 416/476** — 한컴 자체가 PDF 내보내기를 거부한 43건도 숨기지 않고 집계
|
|
167
172
|
- 전체 수치와 주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html) · 기능별 등급: [지원 매트릭스](docs/support-matrix.md)
|
|
@@ -107,10 +107,15 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
107
107
|
|
|
108
108
|
## 실측으로 말합니다
|
|
109
109
|
|
|
110
|
-
만든 파일이 실제 한컴오피스에서
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
- **한컴 열림
|
|
110
|
+
만든 파일이 실제 한컴오피스에서 열리는지 재서 그대로 공개합니다. 측정마다
|
|
111
|
+
스택과 날짜를 병기합니다:
|
|
112
|
+
|
|
113
|
+
- **한컴 열림 120/120 · 렌더 검증 120/120** — 현행 스택(5.7.0 · 실한컴
|
|
114
|
+
12.0.0.3288 · 2026-08-03)의 축소 측정: 기준 스트라텀 + 신규 저작 표면
|
|
115
|
+
6종(각주·누름틀·수식·차트·체크박스·편집 계획). 영수증은 행마다 bucket이
|
|
116
|
+
있어 `jq` 한 줄로 재현됩니다
|
|
117
|
+
- **한컴 열림 476/476** — 전수 동결 코퍼스(N=497) 측정
|
|
118
|
+
(3.4.1 · 실한컴 12.0.0.3288 · 2026-07-19)
|
|
114
119
|
- **미수정 영역 바이트 보존 497/497** · 개인정보 유출 0
|
|
115
120
|
- **렌더 검증 416/476** — 한컴 자체가 PDF 내보내기를 거부한 43건도 숨기지 않고 집계
|
|
116
121
|
- 전체 수치와 주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html) · 기능별 등급: [지원 매트릭스](docs/support-matrix.md)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "5.
|
|
7
|
+
version = "5.8.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -8,8 +8,17 @@
|
|
|
8
8
|
1. 여기 등재된 진입점은 전부 실제로 import 가능해야 한다.
|
|
9
9
|
2. ``editPlanOps``는 :data:`hwpx.plan.PLAN_OPS`·실행 디스패치·edit-plan JSON
|
|
10
10
|
Schema enum과 일치해야 한다.
|
|
11
|
-
3. :data:`_CAPABILITY_AREAS`의
|
|
12
|
-
|
|
11
|
+
3. :data:`_CAPABILITY_AREAS`의 ``authoring_methods``는 라이브
|
|
12
|
+
``HwpxDocument``의 ``add_*`` 전수와 정확히 일치해야 한다 — **레지스트리를
|
|
13
|
+
코드에 대조하는 방향**이다.
|
|
14
|
+
4. 매트릭스 행 제목은 ``docs/support-matrix.md`` 표의 행 제목과 집합 일치해야
|
|
15
|
+
한다.
|
|
16
|
+
|
|
17
|
+
3번이 있는 이유: 4번만으로는 부족하다. 문서 둘이 서로 맞는다는 사실은 코드에
|
|
18
|
+
대해 아무것도 말해주지 않으므로, **양쪽에 다 없는** 능력은 통과한다. 실제로
|
|
19
|
+
``add_check_box``가 5.7.0에서 실한컴 수용 게이트까지 통과해 출하됐는데
|
|
20
|
+
레지스트리에도 매트릭스에도 없었다 — 이 부류의 드리프트를 "구조적으로
|
|
21
|
+
막는다"고 선언한 바로 다음 릴리스에서다. 코드에 대조해야 잡힌다.
|
|
13
22
|
|
|
14
23
|
**core는 환경 변수를 읽지 않는다** — 렌더 오라클 가용성은 여기 없다(자기서술은
|
|
15
24
|
``renderOracle.bundled="none"``으로 그 사실 자체를 말한다). 오라클 탐지·실행은
|
|
@@ -39,21 +48,39 @@ _EXTRA_MODULES: dict[str, tuple[str, ...]] = {
|
|
|
39
48
|
#: 능력 영역 레지스트리. ``matrix_row``는 ``docs/support-matrix.md`` 표 1열
|
|
40
49
|
#: 제목과 **정확히** 일치해야 한다(가드 테스트가 집합 대조). ``entry_points``는
|
|
41
50
|
#: ``"모듈:이름"`` — 전부 import 가능해야 한다(가드 테스트가 해석).
|
|
51
|
+
#: Every ``HwpxDocument.add_*`` method must be claimed by exactly one area.
|
|
52
|
+
#:
|
|
53
|
+
#: ``entry_points`` names modules, which is too coarse to notice a new
|
|
54
|
+
#: authoring method: ten areas all point at ``hwpx.document:HwpxDocument``. So
|
|
55
|
+
#: ``authoring_methods`` names the methods, and the guard in
|
|
56
|
+
#: ``tests/test_capabilities_surface.py`` compares this registry against the
|
|
57
|
+
#: live class.
|
|
58
|
+
#:
|
|
59
|
+
#: Why the guard runs in that direction: the previous guard compared the
|
|
60
|
+
#: registry's row titles against the row titles in ``docs/support-matrix.md``.
|
|
61
|
+
#: Two documents agreeing with each other says nothing about the code, so a
|
|
62
|
+
#: capability missing from *both* passed. ``add_check_box`` shipped in 5.7.0
|
|
63
|
+
#: with a real-Hancom acceptance gate and appeared in neither, one release
|
|
64
|
+
#: after the changelog announced that this class of drift was now structurally
|
|
65
|
+
#: blocked. Comparing against the class is the direction that can catch it.
|
|
42
66
|
_CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
43
67
|
{
|
|
44
68
|
"area": "paragraph-table-authoring",
|
|
45
69
|
"matrix_row": "문단·표 저작/편집",
|
|
46
70
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
71
|
+
"authoring_methods": ("add_paragraph", "add_section"),
|
|
47
72
|
},
|
|
48
73
|
{
|
|
49
74
|
"area": "table-structure",
|
|
50
75
|
"matrix_row": "표 구조 변경(행·열·표 삭제/삽입, 열 오토핏)",
|
|
51
76
|
"entry_points": ("hwpx.table_patch:apply_table_ops",),
|
|
77
|
+
"authoring_methods": (),
|
|
52
78
|
},
|
|
53
79
|
{
|
|
54
80
|
"area": "table-create",
|
|
55
81
|
"matrix_row": "표 생성(병합·중첩 포함)",
|
|
56
82
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
83
|
+
"authoring_methods": ("add_table",),
|
|
57
84
|
},
|
|
58
85
|
{
|
|
59
86
|
"area": "form-fill",
|
|
@@ -63,6 +90,7 @@ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
|
63
90
|
"hwpx.table_patch:fill_cells",
|
|
64
91
|
"hwpx.body_patch:apply_body_ops",
|
|
65
92
|
),
|
|
93
|
+
"authoring_methods": (),
|
|
66
94
|
},
|
|
67
95
|
{
|
|
68
96
|
"area": "edit-plan",
|
|
@@ -71,71 +99,91 @@ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
|
71
99
|
"hwpx.plan:apply_edit_plan",
|
|
72
100
|
"hwpx.plan:validate_edit_plan",
|
|
73
101
|
),
|
|
102
|
+
"authoring_methods": (),
|
|
74
103
|
},
|
|
75
104
|
{
|
|
76
105
|
"area": "shape-authoring",
|
|
77
106
|
"matrix_row": "도형 저작(선·사각형·타원)",
|
|
78
107
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
108
|
+
"authoring_methods": ("add_line", "add_rectangle", "add_ellipse"),
|
|
79
109
|
},
|
|
80
110
|
{
|
|
81
111
|
"area": "shape-escape-hatch",
|
|
82
112
|
"matrix_row": "저수준 도형·컨트롤 탈출구",
|
|
83
113
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
114
|
+
"authoring_methods": ("add_shape", "add_control"),
|
|
84
115
|
},
|
|
85
116
|
{
|
|
86
117
|
"area": "curve-objects",
|
|
87
118
|
"matrix_row": "arc·polygon·curve·connectLine",
|
|
88
119
|
"entry_points": (),
|
|
120
|
+
"authoring_methods": (),
|
|
89
121
|
},
|
|
90
122
|
{
|
|
91
123
|
"area": "picture",
|
|
92
124
|
"matrix_row": "그림 삽입/치환",
|
|
93
125
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
126
|
+
"authoring_methods": ("add_picture", "add_image"),
|
|
94
127
|
},
|
|
95
128
|
{
|
|
96
129
|
"area": "chart",
|
|
97
130
|
"matrix_row": "차트",
|
|
98
131
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
132
|
+
"authoring_methods": ("add_chart",),
|
|
99
133
|
},
|
|
100
134
|
{
|
|
101
135
|
"area": "equation",
|
|
102
136
|
"matrix_row": "수식",
|
|
103
137
|
"entry_points": ("hwpx.equation.authoring:latex_to_eqedit",),
|
|
138
|
+
"authoring_methods": ("add_equation",),
|
|
104
139
|
},
|
|
105
140
|
{
|
|
106
141
|
"area": "redline",
|
|
107
142
|
"matrix_row": "변경추적(redline)",
|
|
108
143
|
"entry_points": ("hwpx.tools.redline:verify_redline",),
|
|
144
|
+
"authoring_methods": ("add_track_change", "add_tracked_insert", "add_tracked_delete", "add_tracked_replace"),
|
|
109
145
|
},
|
|
110
146
|
{
|
|
111
147
|
"area": "memo",
|
|
112
148
|
"matrix_row": "메모(코멘트)",
|
|
113
149
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
150
|
+
"authoring_methods": ("add_memo", "add_memo_with_anchor"),
|
|
114
151
|
},
|
|
115
152
|
{
|
|
116
153
|
"area": "footnote-endnote",
|
|
117
154
|
"matrix_row": "각주/미주",
|
|
118
155
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
156
|
+
"authoring_methods": ("add_footnote", "add_endnote"),
|
|
119
157
|
},
|
|
120
158
|
{
|
|
121
159
|
"area": "toc-crossref",
|
|
122
160
|
"matrix_row": "네이티브 목차(TOC)/상호참조",
|
|
123
161
|
"entry_points": ("hwpx.tools.toc_author:add_native_toc",),
|
|
162
|
+
"authoring_methods": ("add_bookmark", "add_hyperlink"),
|
|
124
163
|
},
|
|
125
164
|
{
|
|
126
165
|
"area": "encrypted-hwpx",
|
|
127
166
|
"matrix_row": "암호화 HWPX",
|
|
128
167
|
"entry_points": (),
|
|
168
|
+
"authoring_methods": (),
|
|
129
169
|
},
|
|
130
170
|
{
|
|
131
171
|
"area": "hwp5-binary",
|
|
132
172
|
"matrix_row": "HWP 5.x 바이너리",
|
|
133
173
|
"entry_points": (),
|
|
174
|
+
"authoring_methods": (),
|
|
134
175
|
},
|
|
135
176
|
{
|
|
136
177
|
"area": "form-field-create",
|
|
137
178
|
"matrix_row": "누름틀(form field) 생성",
|
|
138
179
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
180
|
+
"authoring_methods": ("add_form_field",),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"area": "check-box",
|
|
184
|
+
"matrix_row": "체크박스 양식개체",
|
|
185
|
+
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
186
|
+
"authoring_methods": ("add_check_box",),
|
|
139
187
|
},
|
|
140
188
|
)
|
|
141
189
|
|
|
@@ -458,9 +506,110 @@ def contract_json_schema(name: str) -> dict[str, Any]:
|
|
|
458
506
|
return builder()
|
|
459
507
|
|
|
460
508
|
|
|
509
|
+
def verify_self_description() -> list[str]:
|
|
510
|
+
"""설치된 휠에서 자기서술이 실표면과 맞는지 직접 검사한다.
|
|
511
|
+
|
|
512
|
+
왜 라이브러리 안에 있나: 5.6.0의 CHANGELOG는 이 부류의 드리프트를 "이제
|
|
513
|
+
구조적으로 막습니다"라고 **선언**했고, 바로 다음 릴리스에서 같은 드리프트를
|
|
514
|
+
냈다. 두 번째 선언은 신뢰받을 이유가 없다. 그래서 선언 대신 **누구나 자기
|
|
515
|
+
설치본에서 돌려 볼 수 있는 검사**를 동봉한다 —
|
|
516
|
+
``python -m hwpx.capabilities --verify``.
|
|
517
|
+
|
|
518
|
+
저장소의 테스트가 아니라 설치된 패키지에서 도는 것이 요점이다. 사용자가
|
|
519
|
+
받은 휠이 실제로 무엇을 할 수 있는지 그 휠 자신에게 물어볼 수 있다.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
위반 목록. 빈 리스트면 자기서술이 실표면과 일치한다.
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
from .document import HwpxDocument
|
|
526
|
+
|
|
527
|
+
problems: list[str] = []
|
|
528
|
+
|
|
529
|
+
live = {name for name in dir(HwpxDocument) if name.startswith("add_")}
|
|
530
|
+
registered: dict[str, str] = {}
|
|
531
|
+
for row in _CAPABILITY_AREAS:
|
|
532
|
+
for method in row.get("authoring_methods", ()):
|
|
533
|
+
if method in registered:
|
|
534
|
+
problems.append(
|
|
535
|
+
f"authoring method {method!r} is claimed by both "
|
|
536
|
+
f"{registered[method]!r} and {row['area']!r}"
|
|
537
|
+
)
|
|
538
|
+
registered[method] = row["area"]
|
|
539
|
+
|
|
540
|
+
for method in sorted(live - set(registered)):
|
|
541
|
+
problems.append(
|
|
542
|
+
f"HwpxDocument.{method} exists but no capability area claims it; "
|
|
543
|
+
"the self-description understates what this install can do"
|
|
544
|
+
)
|
|
545
|
+
for method in sorted(set(registered) - live):
|
|
546
|
+
problems.append(
|
|
547
|
+
f"capability registry claims HwpxDocument.{method}, which this "
|
|
548
|
+
"install does not have; the self-description overstates it"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
matrix_rows = {row["matrix_row"] for row in _CAPABILITY_AREAS}
|
|
552
|
+
try:
|
|
553
|
+
text = contract_document("support-matrix")
|
|
554
|
+
except HwpxError as exc: # pragma: no cover - only when the doc is absent
|
|
555
|
+
problems.append(f"bundled support matrix is unreadable: {exc}")
|
|
556
|
+
else:
|
|
557
|
+
documented = {
|
|
558
|
+
line.split("|")[1].strip()
|
|
559
|
+
for line in text.splitlines()
|
|
560
|
+
if line.startswith("|")
|
|
561
|
+
}
|
|
562
|
+
for row in sorted(matrix_rows - documented):
|
|
563
|
+
problems.append(f"capability area {row!r} has no support-matrix row")
|
|
564
|
+
|
|
565
|
+
return problems
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _verify_main(argv: list[str] | None = None) -> int:
|
|
569
|
+
import argparse
|
|
570
|
+
import json as _json
|
|
571
|
+
import sys
|
|
572
|
+
|
|
573
|
+
parser = argparse.ArgumentParser(
|
|
574
|
+
prog="python -m hwpx.capabilities",
|
|
575
|
+
description="Report or verify this install's machine-readable self-description.",
|
|
576
|
+
)
|
|
577
|
+
parser.add_argument(
|
|
578
|
+
"--verify",
|
|
579
|
+
action="store_true",
|
|
580
|
+
help="check the self-description against the installed surface",
|
|
581
|
+
)
|
|
582
|
+
args = parser.parse_args(argv)
|
|
583
|
+
|
|
584
|
+
if not args.verify:
|
|
585
|
+
print(_json.dumps(describe_capabilities(), ensure_ascii=False, indent=2))
|
|
586
|
+
return 0
|
|
587
|
+
|
|
588
|
+
problems = verify_self_description()
|
|
589
|
+
for problem in problems:
|
|
590
|
+
print(f"self-description error: {problem}", file=sys.stderr)
|
|
591
|
+
if problems:
|
|
592
|
+
print(
|
|
593
|
+
f"{len(problems)} self-description problem(s) found in "
|
|
594
|
+
f"python-hwpx {_package_version()}",
|
|
595
|
+
file=sys.stderr,
|
|
596
|
+
)
|
|
597
|
+
return 1
|
|
598
|
+
print(
|
|
599
|
+
f"python-hwpx {_package_version()}: self-description matches the "
|
|
600
|
+
f"installed surface ({len(_CAPABILITY_AREAS)} capability areas)"
|
|
601
|
+
)
|
|
602
|
+
return 0
|
|
603
|
+
|
|
604
|
+
|
|
461
605
|
__all__ = [
|
|
462
606
|
"CAPABILITIES_SCHEMA",
|
|
463
607
|
"describe_capabilities",
|
|
464
608
|
"contract_document",
|
|
465
609
|
"contract_json_schema",
|
|
610
|
+
"verify_self_description",
|
|
466
611
|
]
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
if __name__ == "__main__": # pragma: no cover
|
|
615
|
+
raise SystemExit(_verify_main())
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | corpus-metrics「오픈 수용률」476/476, 「저작 품질 게이트」실저작 58/58, 「렌더 검증」416건 |
|
|
25
25
|
| 표 구조 변경(행·열·표 삭제/삽입, 열 오토핏) | Preserve·Edit | `hwpx.table_patch`; corpus-metrics「바이트 보존」497/497(patch 경로) |
|
|
26
|
-
| 표 생성(병합·중첩 포함) | Create·Render-verified | `add_table`·`merge_cells`(피복 셀 제거+cellSpan — 실한컴 병합 의미론과 동형, 3종 병합 렌더 픽셀 확인)·셀 문단 `add_table`(중첩).
|
|
26
|
+
| 표 생성(병합·중첩 포함) | Create·Render-verified | `add_table`·`merge_cells`(피복 셀 제거+cellSpan — 실한컴 병합 의미론과 동형, 3종 병합 렌더 픽셀 확인)·셀 문단 `add_table`(중첩). 기본값은 5.4.0에서 실한컴에 맞췄다: 셀 안여백 510/510/141/141, 기본 표 폭은 본문 폭, 중첩 표는 부모 셀 사용 폭. **남은 갭**: 선 종류는 raw 전용 |
|
|
27
27
|
| 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch`; corpus-metrics「바이트 보존」497/497. wild 공개 양식의 서식 충실은 2차 실측 후 **산출분 pass 82.1%(23/28)·산출+fail 7.6%(5/66)** — 불가능 타깃은 typed 거부 35건(전수 감별 과잉거부 0). 잔존은 페이지 리플 5건(4건 typed 경고 동반·완전 무음 1건, 「구조결함 2차 실측」절), 표 shape 부류는 측정 인공물로 판명·판정기 교정 |
|
|
28
28
|
| 편집 계획 실행(edit plan) | Preserve·Edit | `hwpx.plan.apply_edit_plan`(experimental, 5.6.0+): 바이트-스플라이스 op 7종을 계획 1파일로 합성 — 정적 선검증→전 체인 인메모리 실행→최종 open-safety 검증→단 1회 원자 쓰기. 중간 실패 시 output·source 바이트 불변(테스트가 바이트 비교로 증명), step별+원본→최종 `hwpx.mutation-report/v1` 실측 사영 |
|
|
29
29
|
| 도형 저작(선·사각형·타원) | Parse·Preserve·Edit·Create·Render-verified | 전용 `add_line`·`add_rectangle`·`add_ellipse`. 5.0.0에서 기하 네임스페이스(`hc:`)·선 bounding box·`resize()`의 기하 반영을 수정한 뒤 실한컴 12.30.0(build 6446)에서 **개봉 7/7**(선 수평·대각·수직, 사각형, 타원, resize 후 혼합, 그림). 렌더로 사각형 144×72pt+`#CCE5FF`, 타원 100×60pt+`#FFD9CC`, 대각선이 요청 박스를 실제 span, `resize(14400,7200)` 후 새 크기·DASH 반영까지 확인 |
|
|
30
30
|
| 저수준 도형·컨트롤 탈출구 | Edit | `add_shape`·`add_control`은 건네받은 요소와 속성만 쓰고 OWPML 필수 하위 요소(`offset`·`orgSz`·`curSz`·`sz`·`pos`·유형별 기하)는 만들지 않는다. 그대로 저장한 문서는 실한컴 12.30.0이 **거부**한다(음성 대조로 확인). 신호는 생성 시점의 `UserWarning` **하나뿐**이다 — 저장을 막지 않고 `validate_package`·`validate_editor_open_safety`도 통과시킨다(실측 `ok=True`). 도형은 위의 전용 헬퍼를 쓸 것(전용 헬퍼는 경고하지 않는다) |
|
|
31
31
|
| arc·polygon·curve·connectLine | Parse·Unsupported-but-preserved | 열거·읽기는 되지만 저작 API 없음. 기존 개체는 patch 저장에서 바이트 동일 왕복 |
|
|
32
32
|
| 그림 삽입/치환 | Edit·Create | `add_picture`·`add_image`·`replace_picture`. 단순 그림 개체 자동 생성은 지원하며 실한컴 개봉을 확인했다(실한컴 코퍼스와 자식 요소 단위 대조). 그룹·효과 등 복잡 개체 생성은 미지원 |
|
|
33
|
-
| 차트 |
|
|
33
|
+
| 차트 | Create(experimental)·Preserve | `add_chart`(5.3.0+): 데이터·계열·축 레이블에서 차트 개체를 생성한다. 기존 차트 part는 patch 저장 시 바이트 보존(497/497). 생성 어휘 밖의 차트 종류·서식은 미지원이며 기존 개체 보존으로만 다룬다 |
|
|
34
34
|
| 수식 | Parse·Create(experimental)·Render-verified | `add_equation`(EqEdit script 삽입, 5.2.0+)과 `hwpx.equation.latex_to_eqedit`(렌더 검증 토큰셋만 변환, 밖은 `UnsupportedLatexError`로 typed 거부). 저작 어휘 전 토큰을 실한컴 렌더 오라클 픽셀 실측(60수식 배터리)으로 확정했고, 기존 수식 개체는 파싱·patch 보존됨(미리보기 MathML 렌더는 뷰어/플러그인 계층) |
|
|
35
35
|
| 변경추적(redline) | Edit·Create | `add_tracked_insert`·`add_tracked_delete`·`add_tracked_replace`; 실 Windows 한컴 COM `IsTrackChange=1`·검토 리본 수락/거부 스파이크. **렌더 주의**: 한컴이 변경추적 문서의 PDF export 자체를 거부 → corpus-metrics「렌더 검증」에서 `render_unavailable`로 정직 집계(결함 아님, 한컴 제약) |
|
|
36
36
|
| 메모(코멘트) | Edit·Create·Render-verified | `add_memo`·`add_memo_with_anchor`; subList 코멘트 텍스트 + `MemoShapeIDRef` 버그 수정을 실 Windows 한컴에서 검증(CHANGELOG) |
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
| 암호화 HWPX | Unsupported-and-rejected | 복호화 API 없음. 암호화된 content part는 파싱 단계에서 예외(`XMLSyntaxError`)로 거부 — 무음으로 잘못된 문서를 만들지 않음(fail-closed) |
|
|
40
40
|
| HWP 5.x 바이너리 | Unsupported-and-rejected | HWP v5는 ZIP이 아니므로 열기 시 `BadZipFile` 예외. OLE2/CFBF 시그니처를 확인하면 예외 메시지가 HWPX 변환을 안내한다(예외 타입은 그대로) |
|
|
41
41
|
| 누름틀(form field) 생성 | Parse·Edit·Create(experimental) | `list_form_fields`·`fill_form_field`로 조회·서식 보존 채움에 더해, `add_form_field`(5.1.0+)가 실한컴 CLICKHERE 계약 그대로 신규 누름틀을 생성한다(표 셀 배치 포함). 만든 필드는 기존 list/fill과 실제 한컴이 특수분기 없이 소비 |
|
|
42
|
+
| 체크박스 양식개체 | Create·Render-verified | `add_check_box`·`list_check_boxes`·`set_check_box`(5.7.0+). 실한컴 실측 계약: `value` CHECKED=☑ / UNCHECKED=□, `<hp:formCharPr>`는 **필수 자식**(없으면 한컴이 문서를 거부하는데 우리 open-safety·ID 무결성은 통과한다 — 실한컴이 유일한 판정자다). 라디오(`hp:radioBtn`)·명령단추(`hp:btn`)는 읽기·보존만 하고 저작 API 없음 |
|
|
42
43
|
|
|
43
44
|
## 상태 판정 근거 요약
|
|
44
45
|
|
|
@@ -104,10 +104,19 @@ class LayoutLintReport:
|
|
|
104
104
|
|
|
105
105
|
from hwpx.quality.report import LayoutReport
|
|
106
106
|
|
|
107
|
+
warnings = [str(f) for f in self.warnings]
|
|
108
|
+
errors = [str(f) for f in self.errors]
|
|
109
|
+
if errors:
|
|
110
|
+
status = "failed"
|
|
111
|
+
elif warnings:
|
|
112
|
+
status = "warned"
|
|
113
|
+
else:
|
|
114
|
+
status = "passed"
|
|
107
115
|
return LayoutReport(
|
|
108
116
|
ok=self.ok,
|
|
109
|
-
warnings=
|
|
110
|
-
errors=
|
|
117
|
+
warnings=warnings,
|
|
118
|
+
errors=errors,
|
|
119
|
+
status=status,
|
|
111
120
|
)
|
|
112
121
|
|
|
113
122
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -95,6 +95,21 @@ class BytePreservingPatchResult:
|
|
|
95
95
|
byte_identical: bool
|
|
96
96
|
zip_method: str
|
|
97
97
|
open_safety: dict[str, Any]
|
|
98
|
+
#: Edits that matched but were thrown away because another patch skipped.
|
|
99
|
+
#:
|
|
100
|
+
#: This family is all-or-nothing: one skip discards the whole set. Those
|
|
101
|
+
#: edits used to be reported in ``applied``, which said the operation had
|
|
102
|
+
#: done something it had not. They are reported here instead, so the
|
|
103
|
+
#: diagnostic value survives without the receipt claiming work.
|
|
104
|
+
discarded: tuple[PatchApplied, ...] = ()
|
|
105
|
+
#: True when ``output_path`` received the unmodified source bytes.
|
|
106
|
+
#:
|
|
107
|
+
#: The byte-splice family writes ``output_path`` even when everything was
|
|
108
|
+
#: skipped, so the file exists and opens and is simply not edited. That is
|
|
109
|
+
#: documented behaviour, kept for consistency across the op family; this
|
|
110
|
+
#: flag makes it impossible to mistake the written file for an edited one
|
|
111
|
+
#: without reading prose.
|
|
112
|
+
output_is_source_copy: bool = False
|
|
98
113
|
# The uniform Phase-B report from the single SavePipeline this byte-path write
|
|
99
114
|
# funnelled through (plan §2 Phase B). Additive and defaulted so existing
|
|
100
115
|
# keyword construction is unaffected.
|
|
@@ -116,6 +131,8 @@ class BytePreservingPatchResult:
|
|
|
116
131
|
"visualComplete": (
|
|
117
132
|
None if self.visual_complete is None else self.visual_complete.to_dict()
|
|
118
133
|
),
|
|
134
|
+
"discarded": [item.to_dict() for item in self.discarded],
|
|
135
|
+
"outputIsSourceCopy": self.output_is_source_copy,
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
def as_mutation_report(self, *, source: bytes | str | Path | None = None) -> MutationReport:
|
|
@@ -223,16 +240,20 @@ def paragraph_patch(
|
|
|
223
240
|
changed_parts[section_path] = updated_xml
|
|
224
241
|
|
|
225
242
|
if skipped:
|
|
243
|
+
# All-or-nothing: one skip discards the whole set. Report the matched
|
|
244
|
+
# edits as discarded rather than applied — nothing was applied.
|
|
226
245
|
open_safety, visual_complete = _finalize(source_bytes, output_path, source=source)
|
|
227
246
|
return BytePreservingPatchResult(
|
|
228
247
|
data=source_bytes,
|
|
229
|
-
applied=
|
|
248
|
+
applied=(),
|
|
230
249
|
skipped=tuple(skipped),
|
|
231
250
|
changed_parts=(),
|
|
232
251
|
byte_identical=True,
|
|
233
252
|
zip_method="skipped",
|
|
234
253
|
open_safety=open_safety,
|
|
235
254
|
visual_complete=visual_complete,
|
|
255
|
+
discarded=tuple(applied),
|
|
256
|
+
output_is_source_copy=output_path is not None,
|
|
236
257
|
)
|
|
237
258
|
|
|
238
259
|
if not changed_parts:
|
|
@@ -126,17 +126,49 @@ class FormReport:
|
|
|
126
126
|
return asdict(self)
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
LayoutStatus = Literal["passed", "warned", "failed", "unverified"]
|
|
130
|
+
|
|
131
|
+
|
|
129
132
|
@dataclass
|
|
130
133
|
class LayoutReport:
|
|
131
|
-
"""Renderer-less layout lint verdict (LayoutLint lands in Phase D).
|
|
134
|
+
"""Renderer-less layout lint verdict (LayoutLint lands in Phase D).
|
|
135
|
+
|
|
136
|
+
``status`` is the explicit tri-state-plus-skip, mirroring
|
|
137
|
+
``visual_complete_status`` above. It exists because ``ok`` alone cannot say
|
|
138
|
+
*why* a report is passing:
|
|
139
|
+
|
|
140
|
+
* ``"passed"`` — the lint ran and found nothing.
|
|
141
|
+
* ``"warned"`` — the lint ran and found non-blocking findings.
|
|
142
|
+
* ``"failed"`` — the lint ran and found a blocking defect.
|
|
143
|
+
* ``"unverified"`` — the lint did **not** run (policy disabled it, or it
|
|
144
|
+
raised). Not a defect, and not evidence of one either.
|
|
145
|
+
|
|
146
|
+
Before ``status`` existed, a disabled gate, a gate that raised, and a real
|
|
147
|
+
pass were all ``LayoutReport.passed()`` and therefore indistinguishable in
|
|
148
|
+
the receipt. A caller who asked for ``layout_lint="strict"`` could receive a
|
|
149
|
+
passing receipt because the linter had thrown. That is the same class of
|
|
150
|
+
error this project names elsewhere as "a score is not a submission": the
|
|
151
|
+
receipt has to say what actually happened.
|
|
152
|
+
|
|
153
|
+
``ok`` keeps its meaning (does this block the save) so behaviour does not
|
|
154
|
+
change; ``unverified`` is non-blocking, exactly as an absent render is.
|
|
155
|
+
"""
|
|
132
156
|
|
|
133
157
|
ok: bool = True
|
|
134
158
|
warnings: list[str] = field(default_factory=list)
|
|
135
159
|
errors: list[str] = field(default_factory=list)
|
|
160
|
+
status: LayoutStatus = "passed"
|
|
161
|
+
unverified_reason: str | None = None
|
|
136
162
|
|
|
137
163
|
@classmethod
|
|
138
164
|
def passed(cls) -> "LayoutReport":
|
|
139
|
-
return cls(ok=True)
|
|
165
|
+
return cls(ok=True, status="passed")
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def unverified(cls, reason: str) -> "LayoutReport":
|
|
169
|
+
"""The lint did not run. Say so instead of reporting a pass."""
|
|
170
|
+
|
|
171
|
+
return cls(ok=True, status="unverified", unverified_reason=reason)
|
|
140
172
|
|
|
141
173
|
def to_dict(self) -> dict[str, Any]:
|
|
142
174
|
return asdict(self)
|
|
@@ -352,7 +352,20 @@ class SavePipeline:
|
|
|
352
352
|
)
|
|
353
353
|
)
|
|
354
354
|
except Exception as exc: # pragma: no cover - defensive: never crash the gate
|
|
355
|
-
|
|
355
|
+
# Never crash the gate, but never launder "could not check" into
|
|
356
|
+
# "checked and fine" either. When the caller explicitly required
|
|
357
|
+
# reference integrity, an unverified check is not a satisfied one.
|
|
358
|
+
reason = f"{type(exc).__name__}: {exc}"
|
|
359
|
+
warnings.append(f"id-integrity check unverified: {reason}")
|
|
360
|
+
if quality.require_reference_integrity:
|
|
361
|
+
ok = False
|
|
362
|
+
errors.append(
|
|
363
|
+
QualityError(
|
|
364
|
+
REFERENCE_INTEGRITY_FAILED,
|
|
365
|
+
"ID/reference integrity was required but could not be "
|
|
366
|
+
f"verified: {reason}",
|
|
367
|
+
)
|
|
368
|
+
)
|
|
356
369
|
return ok
|
|
357
370
|
|
|
358
371
|
def _check_open_safety(
|
|
@@ -390,7 +403,7 @@ class SavePipeline:
|
|
|
390
403
|
"""
|
|
391
404
|
|
|
392
405
|
if quality.layout_lint == "off":
|
|
393
|
-
return LayoutReport.
|
|
406
|
+
return LayoutReport.unverified("layout_lint policy is 'off'")
|
|
394
407
|
|
|
395
408
|
from hwpx.layout.lint import (
|
|
396
409
|
STALE_LINESEG_DETECTED,
|
|
@@ -408,8 +421,12 @@ class SavePipeline:
|
|
|
408
421
|
required_fields=required_fields,
|
|
409
422
|
)
|
|
410
423
|
except Exception as exc: # pragma: no cover - defensive: never crash the gate
|
|
411
|
-
|
|
412
|
-
|
|
424
|
+
# Do not crash the gate, but do not report a pass either. A caller
|
|
425
|
+
# who asked for strict linting and received "passed" because the
|
|
426
|
+
# linter threw has been told something untrue.
|
|
427
|
+
reason = f"layout lint raised {type(exc).__name__}: {exc}"
|
|
428
|
+
warnings.append(f"layout lint unverified: {reason}")
|
|
429
|
+
return LayoutReport.unverified(reason)
|
|
413
430
|
|
|
414
431
|
if quality.layout_lint == "warn":
|
|
415
432
|
lint = lint.demote_errors()
|
|
@@ -13,7 +13,7 @@ cell addressing on top of the same byte machinery:
|
|
|
13
13
|
* resolve a logical ``(row, col)`` -- merge aware -- to the covering ``<hp:tc>``,
|
|
14
14
|
* splice **only** that cell's first paragraph text (reusing
|
|
15
15
|
:func:`hwpx.patch._text_edit_for_paragraph`, which handles empty / self-closing
|
|
16
|
-
``<hp:run/>`` / ``<hp:t/>`` cells -- the
|
|
16
|
+
``<hp:run/>`` / ``<hp:t/>`` cells -- the edge case an early spike hit),
|
|
17
17
|
* strip that paragraph's stale ``<hp:linesegarray>`` so Hancom relayouts,
|
|
18
18
|
* rewrite only the changed section parts through the ZIP partial-patch writer, so
|
|
19
19
|
every untouched byte of the document round-trips identical (Constitution VII).
|
|
@@ -12,11 +12,13 @@ The comparison is at the *part-content* level — the bytes of each ZIP entry
|
|
|
12
12
|
deliberately ignoring ZIP container metadata (entry mtimes, compression method),
|
|
13
13
|
which are not byte-stable across saves and carry no document meaning.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
The *first* serializer output may legitimately differ from the original input
|
|
16
|
+
(we strip stale linesegarray, normalize, etc.), but once through the serializer
|
|
17
|
+
the representation must reach a fixed point — so we compare round-1 output
|
|
18
|
+
against round-2 output, not against the original source. Comparing against the
|
|
19
|
+
source instead would fail every legitimate normalization and prove nothing about
|
|
20
|
+
repeat-save stability, which is the property that actually protects a document
|
|
21
|
+
edited many times.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
24
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -158,10 +158,15 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
158
158
|
|
|
159
159
|
## 실측으로 말합니다
|
|
160
160
|
|
|
161
|
-
만든 파일이 실제 한컴오피스에서
|
|
162
|
-
|
|
161
|
+
만든 파일이 실제 한컴오피스에서 열리는지 재서 그대로 공개합니다. 측정마다
|
|
162
|
+
스택과 날짜를 병기합니다:
|
|
163
163
|
|
|
164
|
-
- **한컴 열림
|
|
164
|
+
- **한컴 열림 120/120 · 렌더 검증 120/120** — 현행 스택(5.7.0 · 실한컴
|
|
165
|
+
12.0.0.3288 · 2026-08-03)의 축소 측정: 기준 스트라텀 + 신규 저작 표면
|
|
166
|
+
6종(각주·누름틀·수식·차트·체크박스·편집 계획). 영수증은 행마다 bucket이
|
|
167
|
+
있어 `jq` 한 줄로 재현됩니다
|
|
168
|
+
- **한컴 열림 476/476** — 전수 동결 코퍼스(N=497) 측정
|
|
169
|
+
(3.4.1 · 실한컴 12.0.0.3288 · 2026-07-19)
|
|
165
170
|
- **미수정 영역 바이트 보존 497/497** · 개인정보 유출 0
|
|
166
171
|
- **렌더 검증 416/476** — 한컴 자체가 PDF 내보내기를 거부한 43건도 숨기지 않고 집계
|
|
167
172
|
- 전체 수치와 주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html) · 기능별 등급: [지원 매트릭스](docs/support-matrix.md)
|