python-hwpx 3.5.0__tar.gz → 3.7.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 (361) hide show
  1. {python_hwpx-3.5.0/src/python_hwpx.egg-info → python_hwpx-3.7.0}/PKG-INFO +83 -8
  2. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/README.md +82 -7
  3. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/pyproject.toml +3 -1
  4. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/__init__.py +6 -0
  5. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/persistence.py +196 -15
  6. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/commands.py +4 -20
  7. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/model.py +56 -0
  8. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/body_patch.py +16 -0
  9. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/document.py +102 -8
  10. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/engine.py +43 -0
  11. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/measure.py +52 -0
  12. python_hwpx-3.7.0/src/hwpx/mutation_report.py +524 -0
  13. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/package.py +66 -0
  14. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/document_parts.py +8 -4
  15. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/paragraph.py +3 -0
  16. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/run.py +4 -0
  17. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/table.py +11 -0
  18. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/patch.py +24 -0
  19. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/table_patch.py +16 -0
  20. {python_hwpx-3.5.0 → python_hwpx-3.7.0/src/python_hwpx.egg-info}/PKG-INFO +83 -8
  21. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/SOURCES.txt +4 -0
  22. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_formatting.py +9 -2
  23. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_save_api.py +4 -1
  24. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit.py +65 -0
  25. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_kordoc_absorption.py +8 -6
  26. python_hwpx-3.7.0/tests/test_layout_cache_scope.py +122 -0
  27. python_hwpx-3.7.0/tests/test_mutation_report.py +323 -0
  28. python_hwpx-3.7.0/tests/test_mutation_report_projections.py +318 -0
  29. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_opc_package.py +38 -6
  30. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_oxml_modularization.py +3 -1
  31. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/LICENSE +0 -0
  32. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/NOTICE +0 -0
  33. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/setup.cfg +0 -0
  34. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/__init__.py +0 -0
  35. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/_units.py +0 -0
  36. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/fields.py +0 -0
  37. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/layout.py +0 -0
  38. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/media.py +0 -0
  39. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/memos.py +0 -0
  40. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/shapes.py +0 -0
  41. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/tracked.py +0 -0
  42. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/__init__.py +0 -0
  43. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/__init__.py +0 -0
  44. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/bundle.py +0 -0
  45. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/catalog.py +0 -0
  46. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/dump.py +0 -0
  47. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/mapping.py +0 -0
  48. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/model.py +0 -0
  49. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/native.py +0 -0
  50. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/replay.py +0 -0
  51. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/catalog.py +0 -0
  52. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/cli.py +0 -0
  53. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/document.py +0 -0
  54. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/form_plan.py +0 -0
  55. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/path.py +0 -0
  56. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/query.py +0 -0
  57. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/story.py +0 -0
  58. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/authoring.py +0 -0
  59. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/benchmark/__init__.py +0 -0
  60. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/benchmark/blind_eval.py +0 -0
  61. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/builder/__init__.py +0 -0
  62. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/builder/core.py +0 -0
  63. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/builder/report.py +0 -0
  64. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/__init__.py +0 -0
  65. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/badges.py +0 -0
  66. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  67. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  68. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  69. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  70. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus.py +0 -0
  71. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/report.py +0 -0
  72. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  73. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/runner.py +0 -0
  74. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  75. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/__init__.py +0 -0
  76. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/_support.py +0 -0
  77. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/composer.py +0 -0
  78. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/harvest.py +0 -0
  79. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/plan.py +0 -0
  80. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profile.py +0 -0
  81. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  82. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  83. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  84. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  85. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  86. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  87. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
  88. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
  89. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
  90. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
  91. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  92. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  93. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  94. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  95. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  96. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  97. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  98. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  99. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  100. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  101. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  102. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  103. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  104. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/validator.py +0 -0
  105. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/evalplan_fill.py +0 -0
  106. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/__init__.py +0 -0
  107. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/compose.py +0 -0
  108. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/ir.py +0 -0
  109. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/measure.py +0 -0
  110. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/parser.py +0 -0
  111. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/profile.py +0 -0
  112. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/fill_residue.py +0 -0
  113. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fill.py +0 -0
  114. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/__init__.py +0 -0
  115. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/apply.py +0 -0
  116. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/policy.py +0 -0
  117. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/report.py +0 -0
  118. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/seal.py +0 -0
  119. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/wordbox.py +0 -0
  120. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/formfill_quality.py +0 -0
  121. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/guidance_scan.py +0 -0
  122. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/ingest/__init__.py +0 -0
  123. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/ingest/base.py +0 -0
  124. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  125. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/layout/__init__.py +0 -0
  126. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/layout/lint.py +0 -0
  127. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/layout/report.py +0 -0
  128. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/relationships.py +0 -0
  129. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/security.py +0 -0
  130. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/xml_utils.py +0 -0
  131. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/__init__.py +0 -0
  132. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/_document_impl.py +0 -0
  133. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/_document_primitives.py +0 -0
  134. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/body.py +0 -0
  135. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  136. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/common.py +0 -0
  137. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/document.py +0 -0
  138. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/header.py +0 -0
  139. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/header_part.py +0 -0
  140. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/memo.py +0 -0
  141. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/namespaces.py +0 -0
  142. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/numbering.py +0 -0
  143. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/objects.py +0 -0
  144. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/parser.py +0 -0
  145. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/schema.py +0 -0
  146. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/section.py +0 -0
  147. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/section_format.py +0 -0
  148. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/section_story.py +0 -0
  149. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/simple_parts.py +0 -0
  150. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/utils.py +0 -0
  151. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/package.py +0 -0
  152. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/presets/__init__.py +0 -0
  153. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/presets/proposal.py +0 -0
  154. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/py.typed +0 -0
  155. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/__init__.py +0 -0
  156. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/ledger.py +0 -0
  157. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/policy.py +0 -0
  158. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/report.py +0 -0
  159. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/save_pipeline.py +0 -0
  160. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/template_formfit.py +0 -0
  161. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/templates.py +0 -0
  162. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/__init__.py +0 -0
  163. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  164. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  165. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/advanced_generators.py +0 -0
  166. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/archive_cli.py +0 -0
  167. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/doc_diff.py +0 -0
  168. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/exporter.py +0 -0
  169. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  170. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  171. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  172. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  173. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  174. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  175. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/generic_inventory.py +0 -0
  176. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/id_integrity.py +0 -0
  177. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/idempotence.py +0 -0
  178. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/ir_equality.py +0 -0
  179. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/layout_preview.py +0 -0
  180. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/mail_merge.py +0 -0
  181. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/markdown_export.py +0 -0
  182. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/object_finder.py +0 -0
  183. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/official_lint.py +0 -0
  184. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/package_reconcile.py +0 -0
  185. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/package_validator.py +0 -0
  186. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/page_guard.py +0 -0
  187. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/pii.py +0 -0
  188. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/read_fidelity.py +0 -0
  189. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/recover.py +0 -0
  190. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/redline.py +0 -0
  191. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/repair.py +0 -0
  192. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/report_parser.py +0 -0
  193. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/report_utils.py +0 -0
  194. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  195. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/style_profile.py +0 -0
  196. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/table_cleanup.py +0 -0
  197. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/table_compute.py +0 -0
  198. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/table_navigation.py +0 -0
  199. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/template_analyzer.py +0 -0
  200. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  201. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/text_extractor.py +0 -0
  202. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/toc_author.py +0 -0
  203. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  204. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/validator.py +0 -0
  205. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/__init__.py +0 -0
  206. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
  207. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
  208. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  209. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  210. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/detectors.py +0 -0
  211. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/diff.py +0 -0
  212. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/fixture_corpus.py +0 -0
  213. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/hancom_worker.py +0 -0
  214. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/masks.py +0 -0
  215. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/oracle.py +0 -0
  216. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/page_qa.py +0 -0
  217. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/qa_contracts.py +0 -0
  218. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/qa_metrics.py +0 -0
  219. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/report.py +0 -0
  220. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  221. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  222. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  223. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  224. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_advanced_generators.py +0 -0
  225. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_blueprint_contracts.py +0 -0
  226. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_blueprint_dump.py +0 -0
  227. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_blueprint_replay.py +0 -0
  228. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_catalog.py +0 -0
  229. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_cli.py +0 -0
  230. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_commands.py +0 -0
  231. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_contracts.py +0 -0
  232. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_document.py +0 -0
  233. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_mixed_form.py +0 -0
  234. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_path_query.py +0 -0
  235. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_story_commands.py +0 -0
  236. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_native_toc.py +0 -0
  237. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_profile_routing.py +0 -0
  238. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
  239. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_quality_corpus.py +0 -0
  240. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_render_check.py +0 -0
  241. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_blind_eval_fixture.py +0 -0
  242. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_body_patch.py +0 -0
  243. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_builder_core.py +0 -0
  244. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_builder_plan_v2.py +0 -0
  245. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_builder_vertical_slice.py +0 -0
  246. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_byte_patch_identity.py +0 -0
  247. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_cell_line_spacing.py +0 -0
  248. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_comment_node_robustness.py +0 -0
  249. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_conformance.py +0 -0
  250. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_corpus_read_fidelity.py +0 -0
  251. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_corpus_toc_verify.py +0 -0
  252. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_coverage_promotion.py +0 -0
  253. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_coverage_targets.py +0 -0
  254. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_design_builder.py +0 -0
  255. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_deviations_registry.py +0 -0
  256. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_doc_diff.py +0 -0
  257. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_context_manager.py +0 -0
  258. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_facade_surface.py +0 -0
  259. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_plan.py +0 -0
  260. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_plan_computed_fields.py +0 -0
  261. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_evalplan_fill.py +0 -0
  262. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_compose.py +0 -0
  263. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_compose_oracle.py +0 -0
  264. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_fixtures.py +0 -0
  265. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_ir.py +0 -0
  266. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_measure.py +0 -0
  267. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_parser.py +0 -0
  268. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_profile.py +0 -0
  269. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_existing_document_format_editing.py +0 -0
  270. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_fill_residue.py +0 -0
  271. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fields.py +0 -0
  272. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fill_split_run.py +0 -0
  273. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_integration.py +0 -0
  274. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_seal.py +0 -0
  275. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_seal_placement.py +0 -0
  276. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_wordbox.py +0 -0
  277. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_formfill_differential_driver.py +0 -0
  278. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_formfill_quality.py +0 -0
  279. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_fuzz_loop.py +0 -0
  280. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_fuzz_regressions.py +0 -0
  281. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_gap_closure_tools.py +0 -0
  282. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_government_report_preset.py +0 -0
  283. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_government_table_profile.py +0 -0
  284. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_guidance_scan.py +0 -0
  285. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_hancom_render_worker.py +0 -0
  286. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_hp_tab_support.py +0 -0
  287. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  288. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_id_generator_range.py +0 -0
  289. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_id_integrity.py +0 -0
  290. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_image_object_workflow.py +0 -0
  291. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_ingest.py +0 -0
  292. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_inline_models.py +0 -0
  293. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  294. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_integration_roundtrip.py +0 -0
  295. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_layout_lint.py +0 -0
  296. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_layout_preview.py +0 -0
  297. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_m9_p0_spike_tools.py +0 -0
  298. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mail_merge_fit.py +0 -0
  299. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mail_merge_table_compute.py +0 -0
  300. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mail_merge_xlsx.py +0 -0
  301. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_markdown_export.py +0 -0
  302. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_memo_and_style_editing.py +0 -0
  303. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mixed_form_plan.py +0 -0
  304. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_namespace_handling.py +0 -0
  305. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_new_features.py +0 -0
  306. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_official_document_style.py +0 -0
  307. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_official_lint_gongmun_gate.py +0 -0
  308. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_official_lint_tableaware.py +0 -0
  309. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_open_safety_corpus.py +0 -0
  310. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_openrate.py +0 -0
  311. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_oxml_parsing.py +0 -0
  312. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_packaging_license_metadata.py +0 -0
  313. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_packaging_py_typed.py +0 -0
  314. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_paragraph_keep_together.py +0 -0
  315. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_paragraph_section_management.py +0 -0
  316. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii.py +0 -0
  317. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii_leak_sweep.py +0 -0
  318. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii_structural.py +0 -0
  319. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii_wiring.py +0 -0
  320. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_proposal_preset.py +0 -0
  321. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_public_runtime_boundary.py +0 -0
  322. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_question_split_detector.py +0 -0
  323. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_read_fidelity.py +0 -0
  324. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_recover_broken_zip.py +0 -0
  325. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_redline_authoring.py +0 -0
  326. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_redline_verify.py +0 -0
  327. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_repair_repack.py +0 -0
  328. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_report_parser.py +0 -0
  329. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_report_utils.py +0 -0
  330. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_repr_snapshots.py +0 -0
  331. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_rhwp_t1_gates.py +0 -0
  332. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_rhwp_t2_verification.py +0 -0
  333. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_roundtrip_fidelity.py +0 -0
  334. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_row_heights.py +0 -0
  335. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_save_pipeline.py +0 -0
  336. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  337. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_section_headers.py +0 -0
  338. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_set_paragraph_format_keep.py +0 -0
  339. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_skeleton_template_ids.py +0 -0
  340. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_split_cell.py +0 -0
  341. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_split_merged_cell.py +0 -0
  342. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_style_profile.py +0 -0
  343. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_cleanup.py +0 -0
  344. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_navigation.py +0 -0
  345. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_patch.py +0 -0
  346. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_patch_dryrun.py +0 -0
  347. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_patch_m105.py +0 -0
  348. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_tables_default_border.py +0 -0
  349. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_template_analyzer_enrichment.py +0 -0
  350. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_template_formfit.py +0 -0
  351. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_text_extractor_annotations.py +0 -0
  352. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_toc_author.py +0 -0
  353. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_toc_fidelity.py +0 -0
  354. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_validation_severity.py +0 -0
  355. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_validator_comment_nodes.py +0 -0
  356. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_version_metadata.py +0 -0
  357. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_fixture_corpus.py +0 -0
  358. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_oracle.py +0 -0
  359. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_oracle_budget.py +0 -0
  360. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_qa_contracts.py +0 -0
  361. {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_qa_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 3.5.0
3
+ Version: 3.7.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -46,7 +46,7 @@ Dynamic: license-file
46
46
  <p align="center">
47
47
  <h1 align="center">python-hwpx</h1>
48
48
  <p align="center">
49
- <strong>한글 없이 HWPX 문서를 Python으로 읽고, 편집하고, 생성하고, 구조를 검증합니다.</strong>
49
+ <strong>한컴 없이 HWPX 안전하게 자동화하는 Python 계층 최소 범위 편집, 검증된 저작, 모든 쓰기에 영수증.</strong>
50
50
  </p>
51
51
  <p align="center">
52
52
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
@@ -56,6 +56,23 @@ Dynamic: license-file
56
56
  </p>
57
57
  </p>
58
58
 
59
+ <p align="center">한국어 | <a href="README_EN.md">English</a></p>
60
+
61
+ ---
62
+
63
+ > **python-hwpx는 한컴 없이 HWPX를 안전하게 자동화하는 Python 계층입니다.** 기존
64
+ > 문서는 최소 범위만 수정하고, 새 문서는 실제 한컴 수용이 검증된 형태로 생성하며,
65
+ > 모든 쓰기에 변경·보존·검증 영수증을 남기고, 완전한 해석과 렌더링은 전문 백엔드에
66
+ > 위임할 수 있습니다.
67
+
68
+ - **최소 범위 편집** — 미수정 part는 저장 시 바이트 그대로 유지됩니다(patch 경로
69
+ 바이트 보존 497/497, 동결 코퍼스 v2 · 2026-07-19).
70
+ - **검증된 저작** — 밑바닥 생성도 실제 한컴이 받아들이는 형태로 냅니다(산출물 한컴
71
+ 오픈 476/476 all-pass, 실저작 품질 게이트 58/58).
72
+ - **모든 쓰기에 영수증** — 대표 저장 경로는 [Safe Write Contract](docs/safe-write-contract.md)의
73
+ `MutationReport`(`hwpx.mutation-report/v1`)로 실제 쓰기 모드·보존 등급·검증 결과를
74
+ **측정해** 반환합니다.
75
+
59
76
  ---
60
77
 
61
78
  ## 🧩 HWPX Stack (3종)
@@ -70,7 +87,7 @@ Dynamic: license-file
70
87
  `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
71
88
  “first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
72
89
 
73
- 현재 PyPI 공개 릴리스는 `python-hwpx 3.5.0`입니다. 일반
90
+ 현재 PyPI 공개 릴리스는 `python-hwpx 3.7.0`입니다. 일반
74
91
  `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
75
92
  현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
76
93
  성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
@@ -80,13 +97,17 @@ Dynamic: license-file
80
97
  ## 실측으로 말합니다 — Published Corpus
81
98
 
82
99
  이 스택의 산출물은 주장 대신 **실제 한컴오피스 전수 측정**으로 검증됩니다
83
- (동결 코퍼스 N=497, 2026-07-19, 상세·주의사항은
100
+ (동결 코퍼스 v2, N=497 산출물, 2026-07-19, 실한컴 12.0.0.3288 COM/GUI 오라클;
101
+ 상세·주의사항은
84
102
  [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)):
85
103
 
86
- - **한컴 오픈 수용률 100%** (476/476 all-pass, 하한 ≥99.4%) · 파싱 96.2%
87
- - **미수정 영역 바이트 보존 100%** (497/497, patch 경로) · **개인정보 0-leak**
88
- - 렌더 검증 416건 + 정직 버킷(변경추적 문서의 PDF export는 한컴 자체가 거부 — 실측 한계로 발행)
89
- - 양식 채움 차등은 wild 공개 양식에서 49.2% — **낮은 숫자도 그대로 발행**하고 잔여 과제로 명기합니다
104
+ - **한컴 오픈 수용률 476/476 all-pass** (동결 코퍼스 v2 · 2026-07-19 · 실한컴 COM
105
+ `Open()` 판정 · rule-of-three 하한 99.37%) · 파싱 96.2%(458/476)
106
+ - **미수정 영역 바이트 보존 497/497** (patch 경로 한정, zip-part diff · 오라클 불요)
107
+ · **개인정보 0-leak** (35문서/합성 140값)
108
+ - 렌더 검증 416/476 (실한컴 `SaveAs("PDF")`) + 정직 버킷 43건(변경추적 문서의 PDF
109
+ export는 한컴 자체가 거부 — 실측 한계로 발행) + 미검증 17건
110
+ - wild 공개 양식 채움은 구조결함 픽스 후 **무음 서식파괴 16.7%**(판정 66조합, 못 담는 타깃은 typed 거부 35건·산출분 pass 17/28) — **낮은 숫자도 그대로 발행**하고 잔여(페이지 리플·표 shape)를 명명합니다
90
111
 
91
112
  > 이 숫자들은 *생성물 수용률* 축입니다(우리가 만든 파일을 실제 한컴이 받아들이는가).
92
113
  > 문서 *파싱 recall*과는 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
@@ -195,6 +216,59 @@ hwpx-analyze-template 보고서.hwpx
195
216
 
196
217
  > 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
197
218
 
219
+ ## 안전한 쓰기 계약 (Safe Write Contract)
220
+
221
+ 대표 저장 경로(`save_to_path` · `save_to_stream` · `to_bytes`)는 **요청한 보존 등급을
222
+ 쓰기 전에 판정하고, 실제로 무엇을 바꿨는지 측정한 영수증**을 돌려줍니다.
223
+
224
+ ```python
225
+ from hwpx.mutation_report import PreservationDowngradeError
226
+
227
+ # 영수증과 함께 저장 — 달성 가능한 가장 강한 보존 등급 자동 선택(mode="auto" 기본)
228
+ report = doc.save_to_path("결과.hwpx", return_report=True)
229
+ print(report.actual_mode) # "patch" | "rebuild"
230
+ print(report.preservation.untouched_part_payloads.to_dict()) # {"verified": 17, "changed": 0}
231
+
232
+ # patch 등급 강제 — 미달이면 아무것도 쓰지 않고 예외(fail-closed)
233
+ try:
234
+ doc.save_to_path("결과.hwpx", mode="patch", fallback="error")
235
+ except PreservationDowngradeError as exc:
236
+ print(exc.offending_parts, exc.suggestion)
237
+ ```
238
+
239
+ - `mode="patch" | "rebuild" | "auto"`(기본 `auto`) · `fallback="error" | "rebuild"`(기본 `error`)
240
+ - `mode="patch"` + `fallback="error"`에서 미수정 part의 바이트 동일성을 지킬 수 없으면
241
+ **아무것도 쓰지 않고** `PreservationDowngradeError`를 던집니다(무음 rebuild 없음).
242
+ - `MutationReport`는 `requestedMode`/`actualMode`/`fallbackUsed`, 변경 part와 좌표 명시
243
+ 범위, 보존 3층(part 페이로드·ZIP 레코드·전체 패키지), 검증 3항목(`passed`/`failed`/`not_performed`)을
244
+ **측정해** 반환합니다.
245
+
246
+ > 파라미터 전체와 `MutationReport` 스키마는 [안전한 쓰기 계약 문서](docs/safe-write-contract.md)를 참고하세요.
247
+
248
+ ## 지원 매트릭스
249
+
250
+ 능력 영역별 실제 등급입니다(동결 코퍼스 v2 · 2026-07-19 · 실한컴 12.0.0.3288 오라클).
251
+ 등급 어휘: **Parse / Preserve / Edit / Create / Render-verified /
252
+ Unsupported-but-preserved / Unsupported-and-rejected**.
253
+
254
+ | 능력 영역 | 상태 | 증거 |
255
+ |---|---|---|
256
+ | 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | 오픈 476/476 · 실저작 게이트 58/58 · 렌더 416 |
257
+ | 표 구조 변경(행·열·표, 오토핏) | Preserve·Edit | `hwpx.table_patch` · 바이트 보존 497/497 |
258
+ | 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch` · 보존 497/497 (wild 무음 서식파괴 16.7%·typed 거부 35/66, 잔여 명명) |
259
+ | 그림 삽입/치환 | Edit·Create | `add_picture`·`replace_picture` (복잡 개체는 한컴 확인 권장) |
260
+ | 차트 | Unsupported-but-preserved | 생성 API 없음 · 기존 차트 part는 patch 보존 |
261
+ | 수식 | Parse·Unsupported-but-preserved | 저작 API 없음 · 기존 수식 파싱·patch 보존 |
262
+ | 변경추적(redline) | Edit·Create | `add_tracked_*` · 실한컴 `IsTrackChange=1` (한컴이 PDF export 거부 → `render_unavailable` 정직 집계) |
263
+ | 메모(코멘트) | Edit·Create·Render-verified | `add_memo*` · 실 Windows 한컴 검증 |
264
+ | 각주/미주 | Edit·Create | `add_footnote`·`add_endnote` (렌더 독립 게이트 미측정) |
265
+ | 네이티브 목차/상호참조 | Create·Render-verified | `add_native_toc`·`toc_verify` · 구조 15/15 · 페이지 정합 5/5 |
266
+ | 암호화 HWPX | Unsupported-and-rejected | 복호화 없음 · 암호화 part는 파싱 단계 예외로 거부 |
267
+ | HWP 5.x 바이너리 | Unsupported-and-rejected | ZIP 아님 → 열기 시 `BadZipFile` (HWPX로 변환 후 사용) |
268
+ | 누름틀(form field) 생성 | Parse·Edit | 기존 필드 조회·서식보존 채움 · **신규 누름틀 생성 도구는 미제공** |
269
+
270
+ > 각 등급의 판정 근거와 상세 증거 포인터는 [지원 매트릭스 문서](docs/support-matrix.md)를 참고하세요.
271
+
198
272
  ## 대항 라이브러리 비교
199
273
 
200
274
  | | python-hwpx | pyhwpx | pyhwp |
@@ -219,6 +293,7 @@ hwpx-analyze-template 보고서.hwpx
219
293
  - **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
220
294
  - **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
221
295
  - **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
296
+ - **[🔬 HWPX 내부 실전 가이드](docs/internals/)** — 실제 한/글 동작에서 확인된 HWPUNIT·조판 캐시·목차 필드·OPC 재패킹·메모·오라클 한계
222
297
  - **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
223
298
  - **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
224
299
 
@@ -1,7 +1,7 @@
1
1
  <p align="center">
2
2
  <h1 align="center">python-hwpx</h1>
3
3
  <p align="center">
4
- <strong>한글 없이 HWPX 문서를 Python으로 읽고, 편집하고, 생성하고, 구조를 검증합니다.</strong>
4
+ <strong>한컴 없이 HWPX 안전하게 자동화하는 Python 계층 최소 범위 편집, 검증된 저작, 모든 쓰기에 영수증.</strong>
5
5
  </p>
6
6
  <p align="center">
7
7
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
@@ -11,6 +11,23 @@
11
11
  </p>
12
12
  </p>
13
13
 
14
+ <p align="center">한국어 | <a href="README_EN.md">English</a></p>
15
+
16
+ ---
17
+
18
+ > **python-hwpx는 한컴 없이 HWPX를 안전하게 자동화하는 Python 계층입니다.** 기존
19
+ > 문서는 최소 범위만 수정하고, 새 문서는 실제 한컴 수용이 검증된 형태로 생성하며,
20
+ > 모든 쓰기에 변경·보존·검증 영수증을 남기고, 완전한 해석과 렌더링은 전문 백엔드에
21
+ > 위임할 수 있습니다.
22
+
23
+ - **최소 범위 편집** — 미수정 part는 저장 시 바이트 그대로 유지됩니다(patch 경로
24
+ 바이트 보존 497/497, 동결 코퍼스 v2 · 2026-07-19).
25
+ - **검증된 저작** — 밑바닥 생성도 실제 한컴이 받아들이는 형태로 냅니다(산출물 한컴
26
+ 오픈 476/476 all-pass, 실저작 품질 게이트 58/58).
27
+ - **모든 쓰기에 영수증** — 대표 저장 경로는 [Safe Write Contract](docs/safe-write-contract.md)의
28
+ `MutationReport`(`hwpx.mutation-report/v1`)로 실제 쓰기 모드·보존 등급·검증 결과를
29
+ **측정해** 반환합니다.
30
+
14
31
  ---
15
32
 
16
33
  ## 🧩 HWPX Stack (3종)
@@ -25,7 +42,7 @@
25
42
  `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
26
43
  “first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
27
44
 
28
- 현재 PyPI 공개 릴리스는 `python-hwpx 3.5.0`입니다. 일반
45
+ 현재 PyPI 공개 릴리스는 `python-hwpx 3.7.0`입니다. 일반
29
46
  `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
30
47
  현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
31
48
  성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
@@ -35,13 +52,17 @@
35
52
  ## 실측으로 말합니다 — Published Corpus
36
53
 
37
54
  이 스택의 산출물은 주장 대신 **실제 한컴오피스 전수 측정**으로 검증됩니다
38
- (동결 코퍼스 N=497, 2026-07-19, 상세·주의사항은
55
+ (동결 코퍼스 v2, N=497 산출물, 2026-07-19, 실한컴 12.0.0.3288 COM/GUI 오라클;
56
+ 상세·주의사항은
39
57
  [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)):
40
58
 
41
- - **한컴 오픈 수용률 100%** (476/476 all-pass, 하한 ≥99.4%) · 파싱 96.2%
42
- - **미수정 영역 바이트 보존 100%** (497/497, patch 경로) · **개인정보 0-leak**
43
- - 렌더 검증 416건 + 정직 버킷(변경추적 문서의 PDF export는 한컴 자체가 거부 — 실측 한계로 발행)
44
- - 양식 채움 차등은 wild 공개 양식에서 49.2% — **낮은 숫자도 그대로 발행**하고 잔여 과제로 명기합니다
59
+ - **한컴 오픈 수용률 476/476 all-pass** (동결 코퍼스 v2 · 2026-07-19 · 실한컴 COM
60
+ `Open()` 판정 · rule-of-three 하한 99.37%) · 파싱 96.2%(458/476)
61
+ - **미수정 영역 바이트 보존 497/497** (patch 경로 한정, zip-part diff · 오라클 불요)
62
+ · **개인정보 0-leak** (35문서/합성 140값)
63
+ - 렌더 검증 416/476 (실한컴 `SaveAs("PDF")`) + 정직 버킷 43건(변경추적 문서의 PDF
64
+ export는 한컴 자체가 거부 — 실측 한계로 발행) + 미검증 17건
65
+ - wild 공개 양식 채움은 구조결함 픽스 후 **무음 서식파괴 16.7%**(판정 66조합, 못 담는 타깃은 typed 거부 35건·산출분 pass 17/28) — **낮은 숫자도 그대로 발행**하고 잔여(페이지 리플·표 shape)를 명명합니다
45
66
 
46
67
  > 이 숫자들은 *생성물 수용률* 축입니다(우리가 만든 파일을 실제 한컴이 받아들이는가).
47
68
  > 문서 *파싱 recall*과는 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
@@ -150,6 +171,59 @@ hwpx-analyze-template 보고서.hwpx
150
171
 
151
172
  > 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
152
173
 
174
+ ## 안전한 쓰기 계약 (Safe Write Contract)
175
+
176
+ 대표 저장 경로(`save_to_path` · `save_to_stream` · `to_bytes`)는 **요청한 보존 등급을
177
+ 쓰기 전에 판정하고, 실제로 무엇을 바꿨는지 측정한 영수증**을 돌려줍니다.
178
+
179
+ ```python
180
+ from hwpx.mutation_report import PreservationDowngradeError
181
+
182
+ # 영수증과 함께 저장 — 달성 가능한 가장 강한 보존 등급 자동 선택(mode="auto" 기본)
183
+ report = doc.save_to_path("결과.hwpx", return_report=True)
184
+ print(report.actual_mode) # "patch" | "rebuild"
185
+ print(report.preservation.untouched_part_payloads.to_dict()) # {"verified": 17, "changed": 0}
186
+
187
+ # patch 등급 강제 — 미달이면 아무것도 쓰지 않고 예외(fail-closed)
188
+ try:
189
+ doc.save_to_path("결과.hwpx", mode="patch", fallback="error")
190
+ except PreservationDowngradeError as exc:
191
+ print(exc.offending_parts, exc.suggestion)
192
+ ```
193
+
194
+ - `mode="patch" | "rebuild" | "auto"`(기본 `auto`) · `fallback="error" | "rebuild"`(기본 `error`)
195
+ - `mode="patch"` + `fallback="error"`에서 미수정 part의 바이트 동일성을 지킬 수 없으면
196
+ **아무것도 쓰지 않고** `PreservationDowngradeError`를 던집니다(무음 rebuild 없음).
197
+ - `MutationReport`는 `requestedMode`/`actualMode`/`fallbackUsed`, 변경 part와 좌표 명시
198
+ 범위, 보존 3층(part 페이로드·ZIP 레코드·전체 패키지), 검증 3항목(`passed`/`failed`/`not_performed`)을
199
+ **측정해** 반환합니다.
200
+
201
+ > 파라미터 전체와 `MutationReport` 스키마는 [안전한 쓰기 계약 문서](docs/safe-write-contract.md)를 참고하세요.
202
+
203
+ ## 지원 매트릭스
204
+
205
+ 능력 영역별 실제 등급입니다(동결 코퍼스 v2 · 2026-07-19 · 실한컴 12.0.0.3288 오라클).
206
+ 등급 어휘: **Parse / Preserve / Edit / Create / Render-verified /
207
+ Unsupported-but-preserved / Unsupported-and-rejected**.
208
+
209
+ | 능력 영역 | 상태 | 증거 |
210
+ |---|---|---|
211
+ | 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | 오픈 476/476 · 실저작 게이트 58/58 · 렌더 416 |
212
+ | 표 구조 변경(행·열·표, 오토핏) | Preserve·Edit | `hwpx.table_patch` · 바이트 보존 497/497 |
213
+ | 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch` · 보존 497/497 (wild 무음 서식파괴 16.7%·typed 거부 35/66, 잔여 명명) |
214
+ | 그림 삽입/치환 | Edit·Create | `add_picture`·`replace_picture` (복잡 개체는 한컴 확인 권장) |
215
+ | 차트 | Unsupported-but-preserved | 생성 API 없음 · 기존 차트 part는 patch 보존 |
216
+ | 수식 | Parse·Unsupported-but-preserved | 저작 API 없음 · 기존 수식 파싱·patch 보존 |
217
+ | 변경추적(redline) | Edit·Create | `add_tracked_*` · 실한컴 `IsTrackChange=1` (한컴이 PDF export 거부 → `render_unavailable` 정직 집계) |
218
+ | 메모(코멘트) | Edit·Create·Render-verified | `add_memo*` · 실 Windows 한컴 검증 |
219
+ | 각주/미주 | Edit·Create | `add_footnote`·`add_endnote` (렌더 독립 게이트 미측정) |
220
+ | 네이티브 목차/상호참조 | Create·Render-verified | `add_native_toc`·`toc_verify` · 구조 15/15 · 페이지 정합 5/5 |
221
+ | 암호화 HWPX | Unsupported-and-rejected | 복호화 없음 · 암호화 part는 파싱 단계 예외로 거부 |
222
+ | HWP 5.x 바이너리 | Unsupported-and-rejected | ZIP 아님 → 열기 시 `BadZipFile` (HWPX로 변환 후 사용) |
223
+ | 누름틀(form field) 생성 | Parse·Edit | 기존 필드 조회·서식보존 채움 · **신규 누름틀 생성 도구는 미제공** |
224
+
225
+ > 각 등급의 판정 근거와 상세 증거 포인터는 [지원 매트릭스 문서](docs/support-matrix.md)를 참고하세요.
226
+
153
227
  ## 대항 라이브러리 비교
154
228
 
155
229
  | | python-hwpx | pyhwpx | pyhwp |
@@ -174,6 +248,7 @@ hwpx-analyze-template 보고서.hwpx
174
248
  - **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
175
249
  - **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
176
250
  - **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
251
+ - **[🔬 HWPX 내부 실전 가이드](docs/internals/)** — 실제 한/글 동작에서 확인된 HWPUNIT·조판 캐시·목차 필드·OPC 재패킹·메모·오라클 한계
177
252
  - **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
178
253
  - **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
179
254
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "3.5.0"
7
+ version = "3.7.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -109,6 +109,7 @@ python_version = "3.10"
109
109
  follow_imports = "skip"
110
110
  files = [
111
111
  "src/hwpx/document.py",
112
+ "src/hwpx/mutation_report.py",
112
113
  "src/hwpx/_document/_units.py",
113
114
  "src/hwpx/_document/fields.py",
114
115
  "src/hwpx/_document/layout.py",
@@ -156,6 +157,7 @@ ignore_missing_imports = true
156
157
  # both central facades and every extracted/touched runtime owner to the gate.
157
158
  include = [
158
159
  "src/hwpx/document.py",
160
+ "src/hwpx/mutation_report.py",
159
161
  "src/hwpx/_document/_units.py",
160
162
  "src/hwpx/_document/fields.py",
161
163
  "src/hwpx/_document/layout.py",
@@ -85,6 +85,10 @@ from .ingest import (
85
85
  HwpxMarkdownConverter,
86
86
  UnsupportedDocumentFormat,
87
87
  )
88
+ from .mutation_report import (
89
+ MutationReport,
90
+ PreservationDowngradeError,
91
+ )
88
92
  from .patch import (
89
93
  BytePreservingPatchResult,
90
94
  ParagraphTextPatch,
@@ -146,6 +150,8 @@ __all__ = [
146
150
  "ParagraphInfo",
147
151
  "PackageValidationReport",
148
152
  "BytePreservingPatchResult",
153
+ "MutationReport",
154
+ "PreservationDowngradeError",
149
155
  "PlanValidationReport",
150
156
  "ParagraphTextPatch",
151
157
  "PatchApplied",
@@ -5,8 +5,18 @@ from __future__ import annotations
5
5
 
6
6
  import warnings
7
7
  from os import PathLike
8
- from typing import TYPE_CHECKING, Any, BinaryIO, Sequence
9
-
8
+ from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Sequence
9
+
10
+ from ..mutation_report import (
11
+ Fallback,
12
+ Mode,
13
+ MutationReport,
14
+ PreservationDowngradeError,
15
+ PreservationMeasurement,
16
+ VerificationSummary,
17
+ VerificationValue,
18
+ measure_save,
19
+ )
10
20
  from ..opc.package import _UNCHECKED_SAVE_TOKEN
11
21
  from ..quality import QualityPolicy
12
22
  from ..quality.report import OpenSafetyReport
@@ -120,27 +130,178 @@ def _gate_and_write(
120
130
  return report
121
131
 
122
132
 
123
- def save_to_path(doc: "HwpxDocument", path: str | PathLike[str]) -> str | PathLike[str]:
124
- """Persist pending changes to *path* and return the same path."""
133
+ def _build_measured(
134
+ doc: "HwpxDocument", *, reset_dirty: bool
135
+ ) -> tuple[bytes, PreservationMeasurement]:
136
+ """Serialize + open-safety validate exactly like ``_to_bytes_raw`` and measure
137
+ the built archive's preservation against the pre-save part payloads.
138
+
139
+ The pre-save snapshot is captured *before* the ``serialize()`` updates are
140
+ applied so a per-save normalizer touching a part the editor never declared
141
+ dirty surfaces as an ``unexpected`` change (Safe Write Contract §2).
142
+ """
143
+
144
+ package = doc._package
145
+ # Measure against the OPEN-TIME baseline, not the current package state: a
146
+ # low-level ``set_part`` applied before this save already lives in
147
+ # ``_files``, and measuring against it would silently launder that change
148
+ # through a ``mode="patch"`` grade (Safe Write Contract §1, fail-closed).
149
+ source_members = dict(
150
+ getattr(package, "_opened_members", None) or package._files
151
+ )
152
+ source_infos = dict(
153
+ getattr(package, "_opened_zip_infos", None) or package._zip_infos
154
+ )
155
+ updates = doc._root.serialize()
156
+ predeclared = set(updates)
157
+ if updates:
158
+ for part_name, payload in updates.items():
159
+ package.set_part(part_name, payload)
160
+ result = package._save_to_bytes(verify_open_safety=True, mark_clean=False)
161
+ if not isinstance(result, bytes):
162
+ raise TypeError("package.save(None) must return bytes")
163
+ doc._run_open_safety_validation(result)
164
+ if reset_dirty:
165
+ _mark_save_clean(doc)
166
+ measurement = measure_save(source_members, source_infos, result, predeclared)
167
+ return result, measurement
168
+
169
+
170
+ def _resolve_grade(
171
+ mode: Mode,
172
+ fallback: Fallback,
173
+ measurement: PreservationMeasurement,
174
+ ) -> tuple[Literal["patch", "rebuild"], bool]:
175
+ """Return ``(actual_mode, fallback_used)``; raise before any write when a
176
+ requested ``patch`` grade is unmet and ``fallback="error"`` (§1)."""
177
+
178
+ if mode == "patch" and not measurement.patch_grade_ok:
179
+ if fallback == "error":
180
+ raise PreservationDowngradeError(
181
+ requested_mode=mode,
182
+ achieved_grade=measurement.achieved_grade,
183
+ offending_parts=measurement.offending_parts,
184
+ suggestion=(
185
+ "route these edits through a byte-preserving primitive "
186
+ "(hwpx.patch/table_patch/body_patch) or pass fallback='rebuild'."
187
+ ),
188
+ )
189
+ return "rebuild", True
190
+ if mode == "auto":
191
+ return measurement.achieved_grade, False
192
+ if mode == "patch":
193
+ return "patch", False
194
+ return "rebuild", False
195
+
196
+
197
+ def _verification_summary(report: "VisualCompleteReport") -> VerificationSummary:
198
+ """Reflect what the pipeline actually ran; never fabricate a pass (§2)."""
199
+
200
+ package: VerificationValue = "passed" if report.ok else "failed"
201
+ open_safety: VerificationValue = "passed" if report.open_safety.ok else "failed"
202
+ # Open-safety validation (which includes the reopen probe) already ran during
203
+ # serialize and raised on failure, so reaching here means reopen passed.
204
+ reopen: VerificationValue = "passed"
205
+ status = report.visual_complete_status
206
+ visual: VerificationValue = (
207
+ "passed"
208
+ if status == "verified"
209
+ else "failed"
210
+ if status == "failed"
211
+ else "not_performed"
212
+ )
213
+ return VerificationSummary(
214
+ package=package, open_safety=open_safety, reopen=reopen, visual=visual
215
+ )
216
+
217
+
218
+ def _compose_mutation_report(
219
+ *,
220
+ path: str | None,
221
+ requested_mode: Mode,
222
+ actual_mode: Literal["patch", "rebuild"],
223
+ fallback_used: bool,
224
+ measurement: PreservationMeasurement,
225
+ report: "VisualCompleteReport",
226
+ ) -> MutationReport:
227
+ return MutationReport(
228
+ requested_mode=requested_mode,
229
+ actual_mode=actual_mode,
230
+ fallback_used=fallback_used,
231
+ changed_parts=measurement.changed_parts,
232
+ preservation=measurement.preservation,
233
+ verification=_verification_summary(report),
234
+ path=path,
235
+ )
236
+
237
+
238
+ def save_to_path(
239
+ doc: "HwpxDocument",
240
+ path: str | PathLike[str],
241
+ *,
242
+ mode: Mode = "auto",
243
+ fallback: Fallback = "error",
244
+ return_report: bool = False,
245
+ ) -> str | PathLike[str] | MutationReport:
246
+ """Persist pending changes to *path*.
247
+
248
+ Returns *path* by default; ``return_report=True`` returns the
249
+ :class:`~hwpx.mutation_report.MutationReport` receipt instead. ``mode`` and
250
+ ``fallback`` are the Safe Write Contract grade controls — ``mode="patch"``
251
+ with ``fallback="error"`` raises :class:`PreservationDowngradeError` and
252
+ writes nothing when the save cannot keep every untouched part byte-identical.
253
+ """
125
254
 
126
255
  _run_pre_save_validation(doc)
127
- archive_bytes = doc._to_bytes_raw(reset_dirty=False)
128
- _gate_and_write(doc,
129
- archive_bytes, output_path=path, source_label="document.save_to_path"
256
+ archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
257
+ actual_mode, fallback_used = _resolve_grade(mode, fallback, measurement)
258
+ report = _gate_and_write(
259
+ doc, archive_bytes, output_path=path, source_label="document.save_to_path"
130
260
  )
131
261
  _mark_save_clean(doc)
262
+ if return_report:
263
+ return _compose_mutation_report(
264
+ path=str(path),
265
+ requested_mode=mode,
266
+ actual_mode=actual_mode,
267
+ fallback_used=fallback_used,
268
+ measurement=measurement,
269
+ report=report,
270
+ )
132
271
  return path
133
272
 
134
273
 
135
- def save_to_stream(doc: "HwpxDocument", stream: BinaryIO) -> BinaryIO:
136
- """Persist pending changes to *stream* and return the same stream."""
274
+ def save_to_stream(
275
+ doc: "HwpxDocument",
276
+ stream: BinaryIO,
277
+ *,
278
+ mode: Mode = "auto",
279
+ fallback: Fallback = "error",
280
+ return_report: bool = False,
281
+ ) -> BinaryIO | MutationReport:
282
+ """Persist pending changes to *stream*.
283
+
284
+ Returns *stream* by default; ``return_report=True`` returns the
285
+ :class:`~hwpx.mutation_report.MutationReport` receipt instead. See
286
+ :func:`save_to_path` for the ``mode``/``fallback`` grade semantics.
287
+ """
137
288
 
138
289
  _run_pre_save_validation(doc)
139
- archive_bytes = doc._to_bytes_raw(reset_dirty=False)
140
- _gate_and_write(doc,
141
- archive_bytes, output_stream=stream, source_label="document.save_to_stream"
290
+ archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
291
+ actual_mode, fallback_used = _resolve_grade(mode, fallback, measurement)
292
+ report = _gate_and_write(
293
+ doc, archive_bytes, output_stream=stream, source_label="document.save_to_stream"
142
294
  )
143
295
  _mark_save_clean(doc)
296
+ if return_report:
297
+ return _compose_mutation_report(
298
+ path=None,
299
+ requested_mode=mode,
300
+ actual_mode=actual_mode,
301
+ fallback_used=fallback_used,
302
+ measurement=measurement,
303
+ report=report,
304
+ )
144
305
  return stream
145
306
 
146
307
 
@@ -193,11 +354,26 @@ def save_report(
193
354
  return report
194
355
 
195
356
 
196
- def to_bytes(doc: "HwpxDocument") -> bytes:
197
- """Serialize pending changes and return the HWPX archive as bytes."""
357
+ def to_bytes(
358
+ doc: "HwpxDocument",
359
+ *,
360
+ mode: Mode = "auto",
361
+ fallback: Fallback = "error",
362
+ ) -> bytes:
363
+ """Serialize pending changes and return the HWPX archive as bytes.
364
+
365
+ ``mode``/``fallback`` enforce the Safe Write Contract grade the same way as
366
+ :func:`save_to_path`: ``mode="patch"`` with ``fallback="error"`` raises
367
+ :class:`PreservationDowngradeError` before returning when the archive is not
368
+ patch-grade. There is no ``return_report`` — the enforcement is via the typed
369
+ exception, so the byte return stays unchanged.
370
+ """
198
371
 
199
372
  _run_pre_save_validation(doc)
200
- return doc._to_bytes_raw()
373
+ archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
374
+ _resolve_grade(mode, fallback, measurement)
375
+ _mark_save_clean(doc)
376
+ return archive_bytes
201
377
 
202
378
 
203
379
  def _to_bytes_raw(
@@ -239,6 +415,11 @@ def _to_bytes_for_validation(doc: "HwpxDocument") -> bytes:
239
415
  def _mark_save_clean(doc: "HwpxDocument") -> None:
240
416
  doc._root.reset_dirty()
241
417
  doc._package.version_info.mark_clean()
418
+ # The published archive is the new preservation baseline for any further
419
+ # save on this document instance.
420
+ package = doc._package
421
+ package._opened_members = dict(package._files)
422
+ package._opened_zip_infos = dict(package._zip_infos)
242
423
 
243
424
 
244
425
  def save(
@@ -14,9 +14,7 @@ import copy
14
14
  import hashlib
15
15
  import json
16
16
  import re
17
- import zipfile
18
17
  from collections.abc import Callable, Mapping, MutableMapping
19
- from io import BytesIO
20
18
  from pathlib import Path
21
19
  from typing import Any
22
20
  from xml.etree import ElementTree as ET
@@ -24,6 +22,7 @@ from xml.etree import ElementTree as ET
24
22
  from lxml import etree as LET # type: ignore[reportAttributeAccessIssue] # lxml has no complete bundled typing
25
23
 
26
24
  from hwpx.document import HwpxDocument
25
+ from hwpx.mutation_report import member_diff_bytes
27
26
  from hwpx.oxml import HwpxOxmlTable
28
27
  from hwpx.quality import QualityPolicy, SavePipeline
29
28
  from hwpx.tools.package_validator import validate_editor_open_safety
@@ -1172,24 +1171,9 @@ def _quality_policy(value: str | Mapping[str, Any] | None) -> QualityPolicy:
1172
1171
 
1173
1172
 
1174
1173
  def _member_diff(before: bytes, after: bytes) -> dict[str, Any]:
1175
- try:
1176
- with zipfile.ZipFile(BytesIO(before)) as old_zip, zipfile.ZipFile(BytesIO(after)) as new_zip:
1177
- old_names = set(old_zip.namelist())
1178
- new_names = set(new_zip.namelist())
1179
- shared = sorted(old_names & new_names)
1180
- changed = [name for name in shared if old_zip.read(name) != new_zip.read(name)]
1181
- unchanged = len(shared) - len(changed)
1182
- return {
1183
- "ok": True,
1184
- "changedMembers": changed,
1185
- "addedMembers": sorted(new_names - old_names),
1186
- "removedMembers": sorted(old_names - new_names),
1187
- "unchangedMemberCount": unchanged,
1188
- "beforeMemberCount": len(old_names),
1189
- "afterMemberCount": len(new_names),
1190
- }
1191
- except (OSError, zipfile.BadZipFile) as exc:
1192
- return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
1174
+ # Shared with the Safe Write Contract's MutationReport spine: one uncompressed
1175
+ # member comparison, one home for the diff shape (mutation_report.py).
1176
+ return member_diff_bytes(before, after)
1193
1177
 
1194
1178
 
1195
1179
  def _verify_header_story_candidates(