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.
Files changed (232) hide show
  1. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/NOTICE +8 -0
  2. {python_hwpx-5.7.0/src/python_hwpx.egg-info → python_hwpx-5.8.0}/PKG-INFO +9 -4
  3. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/README.md +9 -4
  4. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/pyproject.toml +1 -1
  5. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/capabilities.py +151 -2
  6. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/support-matrix.md +3 -2
  7. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/layout/report.py +11 -2
  8. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/patch.py +22 -1
  9. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/report.py +34 -2
  10. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/save_pipeline.py +21 -4
  11. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/table_patch.py +1 -1
  12. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/idempotence.py +7 -5
  13. {python_hwpx-5.7.0 → python_hwpx-5.8.0/src/python_hwpx.egg-info}/PKG-INFO +9 -4
  14. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_capabilities_surface.py +55 -0
  15. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/LICENSE +0 -0
  16. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/setup.cfg +0 -0
  17. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/__init__.py +0 -0
  18. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/__init__.py +0 -0
  19. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/_units.py +0 -0
  20. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/fields.py +0 -0
  21. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/layout.py +0 -0
  22. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/media.py +0 -0
  23. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/memos.py +0 -0
  24. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/persistence.py +0 -0
  25. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/shapes.py +0 -0
  26. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/_document/tracked.py +0 -0
  27. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/body_patch.py +0 -0
  28. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  29. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
  30. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/mutation-semantics.md +0 -0
  31. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/recipes-traversal.md +0 -0
  32. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/document.py +0 -0
  33. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/__init__.py +0 -0
  34. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/authoring.py +0 -0
  35. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/eqedit.py +0 -0
  36. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/mathml.py +0 -0
  37. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/render.py +0 -0
  38. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/equation/tokens.py +0 -0
  39. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/errors.py +0 -0
  40. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/experimental.py +0 -0
  41. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/__init__.py +0 -0
  42. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/apply.py +0 -0
  43. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/engine.py +0 -0
  44. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/measure.py +0 -0
  45. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/policy.py +0 -0
  46. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/report.py +0 -0
  47. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/ingest/__init__.py +0 -0
  48. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/ingest/base.py +0 -0
  49. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  50. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/layout/__init__.py +0 -0
  51. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/layout/lint.py +0 -0
  52. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/mutation_report.py +0 -0
  53. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/package.py +0 -0
  54. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/relationships.py +0 -0
  55. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/security.py +0 -0
  56. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/opc/xml_utils.py +0 -0
  57. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/__init__.py +0 -0
  58. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/_document_impl.py +0 -0
  59. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/_document_primitives.py +0 -0
  60. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/body.py +0 -0
  61. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  62. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/color.py +0 -0
  63. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/common.py +0 -0
  64. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/document.py +0 -0
  65. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/document_parts.py +0 -0
  66. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/header.py +0 -0
  67. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/header_part.py +0 -0
  68. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/memo.py +0 -0
  69. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/namespaces.py +0 -0
  70. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/numbering.py +0 -0
  71. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/objects.py +0 -0
  72. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/paragraph.py +0 -0
  73. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/parser.py +0 -0
  74. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/run.py +0 -0
  75. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/schema.py +0 -0
  76. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section.py +0 -0
  77. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section_format.py +0 -0
  78. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section_story.py +0 -0
  79. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/simple_parts.py +0 -0
  80. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/table.py +0 -0
  81. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/oxml/utils.py +0 -0
  82. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/package.py +0 -0
  83. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/__init__.py +0 -0
  84. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_execute.py +0 -0
  85. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_model.py +0 -0
  86. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_schema.py +0 -0
  87. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/plan/_validate.py +0 -0
  88. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/py.typed +0 -0
  89. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/__init__.py +0 -0
  90. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/ledger.py +0 -0
  91. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/policy.py +0 -0
  92. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/quality/rendering.py +0 -0
  93. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/templates.py +0 -0
  94. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/__init__.py +0 -0
  95. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  96. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  97. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/archive_cli.py +0 -0
  98. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/doc_diff.py +0 -0
  99. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/document_viewer.py +0 -0
  100. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/exporter.py +0 -0
  101. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/generic_inventory.py +0 -0
  102. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/id_integrity.py +0 -0
  103. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/ir_equality.py +0 -0
  104. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/layout_preview.py +0 -0
  105. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/mail_merge.py +0 -0
  106. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/markdown_export.py +0 -0
  107. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/object_finder.py +0 -0
  108. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/package_reconcile.py +0 -0
  109. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/package_validator.py +0 -0
  110. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/page_guard.py +0 -0
  111. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/read_fidelity.py +0 -0
  112. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/recover.py +0 -0
  113. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/redline.py +0 -0
  114. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/repair.py +0 -0
  115. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/report_utils.py +0 -0
  116. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  117. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/table_cleanup.py +0 -0
  118. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/table_navigation.py +0 -0
  119. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/template_analyzer.py +0 -0
  120. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  121. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/text_extractor.py +0 -0
  122. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/toc_author.py +0 -0
  123. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  124. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/hwpx/tools/validator.py +0 -0
  125. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/SOURCES.txt +0 -0
  126. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  127. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  128. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  129. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  130. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_add_control.py +0 -0
  131. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_adoption_metrics_snapshot.py +0 -0
  132. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_authoring_defaults.py +0 -0
  133. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_authoring_quality_corpus.py +0 -0
  134. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_blind_eval_fixture.py +0 -0
  135. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_byte_patch_identity.py +0 -0
  136. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_cell_line_spacing.py +0 -0
  137. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_chart_authoring.py +0 -0
  138. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_check_box_authoring.py +0 -0
  139. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_comment_node_robustness.py +0 -0
  140. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_contract_docs_sync.py +0 -0
  141. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_corpus_read_fidelity.py +0 -0
  142. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_coverage_promotion.py +0 -0
  143. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_coverage_targets.py +0 -0
  144. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_deviations_registry.py +0 -0
  145. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_doc_diff.py +0 -0
  146. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_context_manager.py +0 -0
  147. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_facade_surface.py +0 -0
  148. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_formatting.py +0 -0
  149. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_ops_runtime_boundary.py +0 -0
  150. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_save_api.py +0 -0
  151. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_document_viewer.py +0 -0
  152. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_documentation_code_fences.py +0 -0
  153. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_edit_plan.py +0 -0
  154. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_equation_authoring.py +0 -0
  155. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_equation_converter.py +0 -0
  156. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_equation_render.py +0 -0
  157. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_error_messages.py +0 -0
  158. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_evalplan_core_primitives.py +0 -0
  159. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_existing_document_format_editing.py +0 -0
  160. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_field_authoring.py +0 -0
  161. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fields.py +0 -0
  162. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fill_core_primitives.py +0 -0
  163. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fit.py +0 -0
  164. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_form_fit_integration.py +0 -0
  165. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
  166. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_fuzz_loop.py +0 -0
  167. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_fuzz_regressions.py +0 -0
  168. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_gap_closure_tools.py +0 -0
  169. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_golden_api.py +0 -0
  170. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_hp_tab_support.py +0 -0
  171. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  172. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_id_generator_range.py +0 -0
  173. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_image_object_workflow.py +0 -0
  174. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_ingest.py +0 -0
  175. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_inline_models.py +0 -0
  176. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  177. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_integration_roundtrip.py +0 -0
  178. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_layout_cache_scope.py +0 -0
  179. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_layout_lint.py +0 -0
  180. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_layout_preview.py +0 -0
  181. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_library_boundary_closure.py +0 -0
  182. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_llms_txt.py +0 -0
  183. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_mail_merge_xlsx.py +0 -0
  184. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_markdown_export.py +0 -0
  185. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_memo_and_style_editing.py +0 -0
  186. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_moved_surface_hints.py +0 -0
  187. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_mutation_report.py +0 -0
  188. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_namespace_handling.py +0 -0
  189. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_new_features.py +0 -0
  190. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_note_authoring_contract.py +0 -0
  191. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_opc_package.py +0 -0
  192. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_open_safety_corpus.py +0 -0
  193. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_oxml_modularization.py +0 -0
  194. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_oxml_parsing.py +0 -0
  195. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_packaging_license_metadata.py +0 -0
  196. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_packaging_py_typed.py +0 -0
  197. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_paragraph_keep_together.py +0 -0
  198. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_paragraph_section_management.py +0 -0
  199. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_product_boundary.py +0 -0
  200. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_public_artifact_hygiene.py +0 -0
  201. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_public_runtime_boundary.py +0 -0
  202. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_read_fidelity.py +0 -0
  203. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_recover_broken_zip.py +0 -0
  204. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_redline_authoring.py +0 -0
  205. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_redline_verify.py +0 -0
  206. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_release_workflow_safety.py +0 -0
  207. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_repair_repack.py +0 -0
  208. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_report_utils.py +0 -0
  209. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_repr_snapshots.py +0 -0
  210. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_roundtrip_fidelity.py +0 -0
  211. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_row_heights.py +0 -0
  212. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_save_pipeline.py +0 -0
  213. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_section_headers.py +0 -0
  214. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_set_paragraph_format_keep.py +0 -0
  215. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_shape_geometry_contract.py +0 -0
  216. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_skeleton_template_ids.py +0 -0
  217. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_split_cell.py +0 -0
  218. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_split_merged_cell.py +0 -0
  219. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_stable_surface.py +0 -0
  220. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_cleanup.py +0 -0
  221. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_navigation.py +0 -0
  222. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_patch.py +0 -0
  223. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_patch_dryrun.py +0 -0
  224. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_table_patch_m105.py +0 -0
  225. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_tables_default_border.py +0 -0
  226. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_template_analyzer_enrichment.py +0 -0
  227. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_text_extractor_annotations.py +0 -0
  228. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_toc_author.py +0 -0
  229. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_toc_fidelity.py +0 -0
  230. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_validation_severity.py +0 -0
  231. {python_hwpx-5.7.0 → python_hwpx-5.8.0}/tests/test_validator_comment_nodes.py +0 -0
  232. {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.7.0
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
- 만든 파일이 실제 한컴오피스에서 열리는지, 동결 코퍼스 전수(N=497)를 재서
162
- 그대로 공개합니다:
161
+ 만든 파일이 실제 한컴오피스에서 열리는지 재서 그대로 공개합니다. 측정마다
162
+ 스택과 날짜를 병기합니다:
163
163
 
164
- - **한컴 열림 476/476** — 우리가 만든 파일을 실한컴이 전부 엽니다
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
- 만든 파일이 실제 한컴오피스에서 열리는지, 동결 코퍼스 전수(N=497)를 재서
111
- 그대로 공개합니다:
112
-
113
- - **한컴 열림 476/476** — 우리가 만든 파일을 실한컴이 전부 엽니다
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.0"
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`의 매트릭스 행 제목은 ``docs/support-matrix.md``
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`(중첩). **알려진 기본값 갭**:여백 0(한컴 510/141)·기본 고정·중첩 기본 폭이 부모 셀 초과 렌더 소실 가능(폭 명시 권장)·선 종류는 raw 전용 — 수리 예정 |
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
- | 차트 | Unsupported-but-preserved | 차트 생성 API 없음(kordoc 흡수 갭). 기존 차트 part는 patch 저장 시 바이트 보존(497/497) |
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=[str(f) for f in self.warnings],
110
- errors=[str(f) for f in self.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=tuple(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
- warnings.append(f"id-integrity check skipped: {type(exc).__name__}: {exc}")
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.passed()
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
- warnings.append(f"layout lint skipped: {type(exc).__name__}: {exc}")
412
- return LayoutReport.passed()
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 kordoc #4/#30 edge the P0 spike hit),
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
- Following the rhwp roundtrip doctrine: the *first* serializer output may
16
- legitimately differ from the original input (we strip stale linesegarray,
17
- normalize, etc.), but once through the serializer the representation must reach
18
- a fixed point — so we compare round-1 output against round-2 output, not against
19
- the original source.
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.7.0
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
- 만든 파일이 실제 한컴오피스에서 열리는지, 동결 코퍼스 전수(N=497)를 재서
162
- 그대로 공개합니다:
161
+ 만든 파일이 실제 한컴오피스에서 열리는지 재서 그대로 공개합니다. 측정마다
162
+ 스택과 날짜를 병기합니다:
163
163
 
164
- - **한컴 열림 476/476** — 우리가 만든 파일을 실한컴이 전부 엽니다
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)