python-hwpx 3.5.0__tar.gz → 3.6.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 (358) hide show
  1. {python_hwpx-3.5.0/src/python_hwpx.egg-info → python_hwpx-3.6.0}/PKG-INFO +2 -2
  2. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/README.md +1 -1
  3. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/pyproject.toml +1 -1
  4. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/engine.py +43 -0
  5. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/measure.py +52 -0
  6. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/package.py +59 -0
  7. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/document_parts.py +8 -4
  8. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/paragraph.py +3 -0
  9. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/run.py +4 -0
  10. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/table.py +11 -0
  11. {python_hwpx-3.5.0 → python_hwpx-3.6.0/src/python_hwpx.egg-info}/PKG-INFO +2 -2
  12. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
  13. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_formatting.py +9 -2
  14. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_save_api.py +4 -1
  15. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit.py +65 -0
  16. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_kordoc_absorption.py +8 -6
  17. python_hwpx-3.6.0/tests/test_layout_cache_scope.py +122 -0
  18. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_opc_package.py +38 -6
  19. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/LICENSE +0 -0
  20. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/NOTICE +0 -0
  21. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/setup.cfg +0 -0
  22. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/__init__.py +0 -0
  23. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/__init__.py +0 -0
  24. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/_units.py +0 -0
  25. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/fields.py +0 -0
  26. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/layout.py +0 -0
  27. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/media.py +0 -0
  28. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/memos.py +0 -0
  29. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/persistence.py +0 -0
  30. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/shapes.py +0 -0
  31. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/tracked.py +0 -0
  32. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/__init__.py +0 -0
  33. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/__init__.py +0 -0
  34. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/bundle.py +0 -0
  35. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/catalog.py +0 -0
  36. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/dump.py +0 -0
  37. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/mapping.py +0 -0
  38. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/model.py +0 -0
  39. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/native.py +0 -0
  40. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/replay.py +0 -0
  41. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/catalog.py +0 -0
  42. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/cli.py +0 -0
  43. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/commands.py +0 -0
  44. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/document.py +0 -0
  45. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/form_plan.py +0 -0
  46. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/model.py +0 -0
  47. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/path.py +0 -0
  48. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/query.py +0 -0
  49. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/story.py +0 -0
  50. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/authoring.py +0 -0
  51. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/benchmark/__init__.py +0 -0
  52. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/benchmark/blind_eval.py +0 -0
  53. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/body_patch.py +0 -0
  54. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/builder/__init__.py +0 -0
  55. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/builder/core.py +0 -0
  56. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/builder/report.py +0 -0
  57. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/__init__.py +0 -0
  58. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/badges.py +0 -0
  59. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  60. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  61. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  62. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  63. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus.py +0 -0
  64. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/report.py +0 -0
  65. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  66. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/runner.py +0 -0
  67. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  68. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/__init__.py +0 -0
  69. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/_support.py +0 -0
  70. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/composer.py +0 -0
  71. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/harvest.py +0 -0
  72. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/plan.py +0 -0
  73. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profile.py +0 -0
  74. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  75. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  76. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  77. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  78. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  79. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  80. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
  81. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
  82. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
  83. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
  84. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  85. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  86. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  87. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  88. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  89. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  90. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  91. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  92. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  93. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  94. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  95. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  96. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  97. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/validator.py +0 -0
  98. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/document.py +0 -0
  99. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/evalplan_fill.py +0 -0
  100. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/__init__.py +0 -0
  101. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/compose.py +0 -0
  102. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/ir.py +0 -0
  103. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/measure.py +0 -0
  104. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/parser.py +0 -0
  105. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/profile.py +0 -0
  106. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/fill_residue.py +0 -0
  107. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fill.py +0 -0
  108. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/__init__.py +0 -0
  109. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/apply.py +0 -0
  110. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/policy.py +0 -0
  111. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/report.py +0 -0
  112. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/seal.py +0 -0
  113. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/wordbox.py +0 -0
  114. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/formfill_quality.py +0 -0
  115. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/guidance_scan.py +0 -0
  116. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/ingest/__init__.py +0 -0
  117. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/ingest/base.py +0 -0
  118. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  119. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/layout/__init__.py +0 -0
  120. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/layout/lint.py +0 -0
  121. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/layout/report.py +0 -0
  122. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/relationships.py +0 -0
  123. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/security.py +0 -0
  124. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/xml_utils.py +0 -0
  125. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/__init__.py +0 -0
  126. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/_document_impl.py +0 -0
  127. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/_document_primitives.py +0 -0
  128. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/body.py +0 -0
  129. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  130. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/common.py +0 -0
  131. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/document.py +0 -0
  132. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/header.py +0 -0
  133. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/header_part.py +0 -0
  134. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/memo.py +0 -0
  135. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/namespaces.py +0 -0
  136. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/numbering.py +0 -0
  137. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/objects.py +0 -0
  138. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/parser.py +0 -0
  139. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/schema.py +0 -0
  140. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/section.py +0 -0
  141. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/section_format.py +0 -0
  142. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/section_story.py +0 -0
  143. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/simple_parts.py +0 -0
  144. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/utils.py +0 -0
  145. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/package.py +0 -0
  146. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/patch.py +0 -0
  147. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/presets/__init__.py +0 -0
  148. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/presets/proposal.py +0 -0
  149. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/py.typed +0 -0
  150. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/__init__.py +0 -0
  151. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/ledger.py +0 -0
  152. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/policy.py +0 -0
  153. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/report.py +0 -0
  154. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/save_pipeline.py +0 -0
  155. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/table_patch.py +0 -0
  156. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/template_formfit.py +0 -0
  157. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/templates.py +0 -0
  158. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/__init__.py +0 -0
  159. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  160. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  161. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/advanced_generators.py +0 -0
  162. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/archive_cli.py +0 -0
  163. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/doc_diff.py +0 -0
  164. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/exporter.py +0 -0
  165. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  166. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  167. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  168. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  169. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  170. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  171. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/generic_inventory.py +0 -0
  172. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/id_integrity.py +0 -0
  173. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/idempotence.py +0 -0
  174. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/ir_equality.py +0 -0
  175. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/layout_preview.py +0 -0
  176. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/mail_merge.py +0 -0
  177. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/markdown_export.py +0 -0
  178. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/object_finder.py +0 -0
  179. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/official_lint.py +0 -0
  180. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/package_reconcile.py +0 -0
  181. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/package_validator.py +0 -0
  182. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/page_guard.py +0 -0
  183. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/pii.py +0 -0
  184. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/read_fidelity.py +0 -0
  185. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/recover.py +0 -0
  186. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/redline.py +0 -0
  187. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/repair.py +0 -0
  188. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/report_parser.py +0 -0
  189. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/report_utils.py +0 -0
  190. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  191. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/style_profile.py +0 -0
  192. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/table_cleanup.py +0 -0
  193. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/table_compute.py +0 -0
  194. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/table_navigation.py +0 -0
  195. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/template_analyzer.py +0 -0
  196. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  197. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/text_extractor.py +0 -0
  198. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/toc_author.py +0 -0
  199. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  200. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/validator.py +0 -0
  201. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/__init__.py +0 -0
  202. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
  203. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
  204. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  205. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  206. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/detectors.py +0 -0
  207. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/diff.py +0 -0
  208. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/fixture_corpus.py +0 -0
  209. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/hancom_worker.py +0 -0
  210. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/masks.py +0 -0
  211. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/oracle.py +0 -0
  212. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/page_qa.py +0 -0
  213. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/qa_contracts.py +0 -0
  214. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/qa_metrics.py +0 -0
  215. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/report.py +0 -0
  216. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  217. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  218. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  219. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  220. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_advanced_generators.py +0 -0
  221. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_blueprint_contracts.py +0 -0
  222. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_blueprint_dump.py +0 -0
  223. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_blueprint_replay.py +0 -0
  224. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_catalog.py +0 -0
  225. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_cli.py +0 -0
  226. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_commands.py +0 -0
  227. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_contracts.py +0 -0
  228. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_document.py +0 -0
  229. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_mixed_form.py +0 -0
  230. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_path_query.py +0 -0
  231. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_story_commands.py +0 -0
  232. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_native_toc.py +0 -0
  233. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_profile_routing.py +0 -0
  234. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
  235. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_quality_corpus.py +0 -0
  236. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_render_check.py +0 -0
  237. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_blind_eval_fixture.py +0 -0
  238. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_body_patch.py +0 -0
  239. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_builder_core.py +0 -0
  240. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_builder_plan_v2.py +0 -0
  241. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_builder_vertical_slice.py +0 -0
  242. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_byte_patch_identity.py +0 -0
  243. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_cell_line_spacing.py +0 -0
  244. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_comment_node_robustness.py +0 -0
  245. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_conformance.py +0 -0
  246. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_corpus_read_fidelity.py +0 -0
  247. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_corpus_toc_verify.py +0 -0
  248. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_coverage_promotion.py +0 -0
  249. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_coverage_targets.py +0 -0
  250. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_design_builder.py +0 -0
  251. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_deviations_registry.py +0 -0
  252. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_doc_diff.py +0 -0
  253. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_context_manager.py +0 -0
  254. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_facade_surface.py +0 -0
  255. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_plan.py +0 -0
  256. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_plan_computed_fields.py +0 -0
  257. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_evalplan_fill.py +0 -0
  258. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_compose.py +0 -0
  259. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_compose_oracle.py +0 -0
  260. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_fixtures.py +0 -0
  261. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_ir.py +0 -0
  262. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_measure.py +0 -0
  263. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_parser.py +0 -0
  264. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_profile.py +0 -0
  265. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_existing_document_format_editing.py +0 -0
  266. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_fill_residue.py +0 -0
  267. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fields.py +0 -0
  268. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fill_split_run.py +0 -0
  269. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_integration.py +0 -0
  270. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_seal.py +0 -0
  271. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_seal_placement.py +0 -0
  272. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_wordbox.py +0 -0
  273. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_formfill_differential_driver.py +0 -0
  274. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_formfill_quality.py +0 -0
  275. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_fuzz_loop.py +0 -0
  276. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_fuzz_regressions.py +0 -0
  277. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_gap_closure_tools.py +0 -0
  278. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_government_report_preset.py +0 -0
  279. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_government_table_profile.py +0 -0
  280. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_guidance_scan.py +0 -0
  281. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_hancom_render_worker.py +0 -0
  282. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_hp_tab_support.py +0 -0
  283. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  284. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_id_generator_range.py +0 -0
  285. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_id_integrity.py +0 -0
  286. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_image_object_workflow.py +0 -0
  287. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_ingest.py +0 -0
  288. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_inline_models.py +0 -0
  289. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  290. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_integration_roundtrip.py +0 -0
  291. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_layout_lint.py +0 -0
  292. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_layout_preview.py +0 -0
  293. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_m9_p0_spike_tools.py +0 -0
  294. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mail_merge_fit.py +0 -0
  295. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mail_merge_table_compute.py +0 -0
  296. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mail_merge_xlsx.py +0 -0
  297. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_markdown_export.py +0 -0
  298. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_memo_and_style_editing.py +0 -0
  299. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mixed_form_plan.py +0 -0
  300. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_namespace_handling.py +0 -0
  301. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_new_features.py +0 -0
  302. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_official_document_style.py +0 -0
  303. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_official_lint_gongmun_gate.py +0 -0
  304. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_official_lint_tableaware.py +0 -0
  305. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_open_safety_corpus.py +0 -0
  306. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_openrate.py +0 -0
  307. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_oxml_modularization.py +0 -0
  308. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_oxml_parsing.py +0 -0
  309. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_packaging_license_metadata.py +0 -0
  310. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_packaging_py_typed.py +0 -0
  311. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_paragraph_keep_together.py +0 -0
  312. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_paragraph_section_management.py +0 -0
  313. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii.py +0 -0
  314. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii_leak_sweep.py +0 -0
  315. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii_structural.py +0 -0
  316. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii_wiring.py +0 -0
  317. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_proposal_preset.py +0 -0
  318. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_public_runtime_boundary.py +0 -0
  319. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_question_split_detector.py +0 -0
  320. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_read_fidelity.py +0 -0
  321. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_recover_broken_zip.py +0 -0
  322. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_redline_authoring.py +0 -0
  323. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_redline_verify.py +0 -0
  324. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_repair_repack.py +0 -0
  325. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_report_parser.py +0 -0
  326. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_report_utils.py +0 -0
  327. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_repr_snapshots.py +0 -0
  328. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_rhwp_t1_gates.py +0 -0
  329. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_rhwp_t2_verification.py +0 -0
  330. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_roundtrip_fidelity.py +0 -0
  331. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_row_heights.py +0 -0
  332. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_save_pipeline.py +0 -0
  333. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  334. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_section_headers.py +0 -0
  335. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_set_paragraph_format_keep.py +0 -0
  336. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_skeleton_template_ids.py +0 -0
  337. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_split_cell.py +0 -0
  338. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_split_merged_cell.py +0 -0
  339. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_style_profile.py +0 -0
  340. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_cleanup.py +0 -0
  341. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_navigation.py +0 -0
  342. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_patch.py +0 -0
  343. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_patch_dryrun.py +0 -0
  344. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_patch_m105.py +0 -0
  345. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_tables_default_border.py +0 -0
  346. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_template_analyzer_enrichment.py +0 -0
  347. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_template_formfit.py +0 -0
  348. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_text_extractor_annotations.py +0 -0
  349. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_toc_author.py +0 -0
  350. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_toc_fidelity.py +0 -0
  351. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_validation_severity.py +0 -0
  352. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_validator_comment_nodes.py +0 -0
  353. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_version_metadata.py +0 -0
  354. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_fixture_corpus.py +0 -0
  355. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_oracle.py +0 -0
  356. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_oracle_budget.py +0 -0
  357. {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_qa_contracts.py +0 -0
  358. {python_hwpx-3.5.0 → python_hwpx-3.6.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.6.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -70,7 +70,7 @@ Dynamic: license-file
70
70
  `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
71
71
  “first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
72
72
 
73
- 현재 PyPI 공개 릴리스는 `python-hwpx 3.5.0`입니다. 일반
73
+ 현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
74
74
  `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
75
75
  현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
76
76
  성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
@@ -25,7 +25,7 @@
25
25
  `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
26
26
  “first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
27
27
 
28
- 현재 PyPI 공개 릴리스는 `python-hwpx 3.5.0`입니다. 일반
28
+ 현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
29
29
  `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
30
30
  현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
31
31
  성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
@@ -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.6.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -55,6 +55,26 @@ class FitEngine:
55
55
  policy: FitPolicy | None = None,
56
56
  *,
57
57
  field_id: str | None = None,
58
+ ) -> FitResult:
59
+ result = self._fit_decide(value, slot, policy, field_id=field_id)
60
+ inline_count = int(getattr(slot, "inline_object_count", 0) or 0)
61
+ if inline_count and (value or "") != "":
62
+ inline_width = float(getattr(slot, "inline_object_width", 0.0) or 0.0)
63
+ result.warnings.append(
64
+ f"cell line shared with {inline_count} inline control(s) "
65
+ f"({inline_width:.0f} HWPUNIT deducted from the width budget); "
66
+ "control-sharing cells are unusual fill targets — verify with a "
67
+ "render gate or pick the intended value cell"
68
+ )
69
+ return result
70
+
71
+ def _fit_decide(
72
+ self,
73
+ value: str,
74
+ slot: SlotMetrics,
75
+ policy: FitPolicy | None = None,
76
+ *,
77
+ field_id: str | None = None,
58
78
  ) -> FitResult:
59
79
  policy = policy or FitPolicy()
60
80
  value = "" if value is None else str(value)
@@ -77,6 +97,29 @@ class FitEngine:
77
97
  if policy.mode == "expand_row":
78
98
  return self._expand_row(value, slot, policy, field_id)
79
99
 
100
+ # Inline objects can consume the whole line; with less than a glyph of
101
+ # width left there is nothing to reshape into — refuse with a message
102
+ # that names the cause instead of an absurd wrapped-line estimate.
103
+ if (
104
+ int(getattr(slot, "inline_object_count", 0) or 0)
105
+ and slot.available_width < slot.font_pt * 100.0 * 0.6
106
+ ):
107
+ return FitResult(
108
+ ok=False,
109
+ value=value,
110
+ applied_value=value,
111
+ lines=0,
112
+ font_pt=slot.font_pt,
113
+ overflow_detected=True,
114
+ confidence="high",
115
+ errors=[
116
+ "FIELD_OVERFLOW: inline control(s) leave no usable width in "
117
+ "this cell; choose the intended value cell or allow row "
118
+ "expansion"
119
+ ],
120
+ field_id=field_id,
121
+ )
122
+
80
123
  # Reshaping ladder: wrap, shrink, or wrap_then_shrink, then fail_on_overflow.
81
124
  wrap_lines = policy.effective_max_lines if policy.may_wrap else 1
82
125
  wrap_slot = replace(slot, max_lines=wrap_lines)
@@ -221,6 +221,13 @@ class SlotMetrics:
221
221
  # Per-line advance as a multiple of the em, from the cell's declared paragraph
222
222
  # line spacing (PERCENT). ``None`` falls back to ``DEFAULT_LINE_SPACING_RATIO``.
223
223
  line_spacing_ratio: float | None = None
224
+ # Width already consumed on the line by inline treat-as-char objects
225
+ # (checkboxes, form controls, pictures) that share the target paragraph.
226
+ # Their declared ``hp:sz/@width`` is subtracted from the usable width —
227
+ # ignoring them made the engine call "fits" on a fill that real Hancom
228
+ # wrapped, growing the row and repaginating a 10-page form (S-087 P0).
229
+ inline_object_width: float = 0.0
230
+ inline_object_count: int = 0
224
231
  # A cell height existed but was unusable (merged row-span fragment, or an
225
232
  # auto-grow floor shorter than one line). Records "height budget unavailable"
226
233
  # so the fit reports width-only honestly rather than guessing a vertical fit.
@@ -473,6 +480,11 @@ def resolve_slot_metrics(
473
480
  element = getattr(cell, "element", None)
474
481
  left, right = _cell_margin(element) if element is not None else (0, 0)
475
482
  inner = max(raw_width - left - right, 0.0) * safety
483
+ inline_width, inline_count = (
484
+ _inline_object_width(element) if element is not None else (0.0, 0)
485
+ )
486
+ if inline_width:
487
+ inner = max(inner - inline_width, 0.0)
476
488
  resolved_pt = font_pt if font_pt is not None else _first_run_font_pt(cell, document)
477
489
  line_ratio = _first_para_line_spacing_ratio(cell, document)
478
490
 
@@ -503,9 +515,49 @@ def resolve_slot_metrics(
503
515
  available_height=available_height,
504
516
  line_spacing_ratio=line_ratio,
505
517
  height_unavailable=height_unavailable,
518
+ inline_object_width=inline_width,
519
+ inline_object_count=inline_count,
506
520
  )
507
521
 
508
522
 
523
+ def _inline_object_width(cell_element: object) -> tuple[float, int]:
524
+ """Total declared width of inline treat-as-char objects in the cell.
525
+
526
+ Checkboxes and similar form controls flow on the text line
527
+ (``hp:pos/@treatAsChar='1'``) and consume their ``hp:sz/@width``; a fill
528
+ value shares whatever width remains. Objects without a declared size are
529
+ counted but contribute 0 width (the count still signals reduced trust).
530
+ """
531
+
532
+ total = 0.0
533
+ count = 0
534
+ iter_fn = getattr(cell_element, "iter", None)
535
+ if iter_fn is None:
536
+ return 0.0, 0
537
+ for node in iter_fn():
538
+ tag = getattr(node, "tag", "")
539
+ if not isinstance(tag, str):
540
+ continue
541
+ local = tag.rsplit("}", 1)[-1].lower()
542
+ if local != "pos":
543
+ continue
544
+ if (node.get("treatAsChar") or "").strip() not in {"1", "true", "TRUE"}:
545
+ continue
546
+ holder = node.getparent() if hasattr(node, "getparent") else None
547
+ if holder is None:
548
+ continue
549
+ count += 1
550
+ for sibling in holder:
551
+ sib_local = str(getattr(sibling, "tag", "")).rsplit("}", 1)[-1].lower()
552
+ if sib_local == "sz":
553
+ try:
554
+ total += float(sibling.get("width") or 0)
555
+ except (TypeError, ValueError):
556
+ pass
557
+ break
558
+ return total, count
559
+
560
+
509
561
  __all__ = [
510
562
  "SlotMetrics",
511
563
  "Measurement",
@@ -159,10 +159,69 @@ def _local_name(element: etree._Element) -> str:
159
159
  return tag
160
160
 
161
161
 
162
+ def _paragraph_plain_text_length(paragraph: etree._Element) -> int | None:
163
+ """Plain text length of a simple paragraph, or ``None`` when unjudgeable.
164
+
165
+ Mirrors the oxml-side stale detector: only paragraphs made purely of runs
166
+ with text/tab-like children can be judged at the byte boundary; anything
167
+ else (controls, tables, fields) returns ``None`` so its cache is kept.
168
+ """
169
+ total = 0
170
+ for child in paragraph:
171
+ child_name = _local_name(child).lower()
172
+ if child_name in _LAYOUT_CACHE_ELEMENT_NAMES:
173
+ continue
174
+ if child_name != "run":
175
+ return None
176
+ for run_child in child:
177
+ run_child_name = _local_name(run_child).lower()
178
+ if run_child_name == "t":
179
+ total += len("".join(run_child.itertext()))
180
+ elif run_child_name in {"tab", "linebreak", "hyphen", "nbspace"}:
181
+ total += 1
182
+ else:
183
+ return None
184
+ return total
185
+
186
+
187
+ def _paragraph_layout_cache_is_stale(paragraph: etree._Element) -> bool:
188
+ text_length = _paragraph_plain_text_length(paragraph)
189
+ if text_length is None:
190
+ return False
191
+ for child in paragraph:
192
+ if _local_name(child).lower() not in _LAYOUT_CACHE_ELEMENT_NAMES:
193
+ continue
194
+ for line_seg in child:
195
+ if _local_name(line_seg).lower() != "lineseg":
196
+ continue
197
+ textpos = line_seg.get("textpos")
198
+ if textpos is None:
199
+ continue
200
+ try:
201
+ if int(textpos) > text_length:
202
+ return True
203
+ except ValueError:
204
+ return True
205
+ return False
206
+
207
+
162
208
  def _strip_section_layout_caches(payload: bytes) -> bytes:
209
+ """Drop only *stale* paragraph layout caches from a section payload.
210
+
211
+ Valid caches are the authored layout of untouched paragraphs; removing
212
+ them forces the editor to re-lay-out entire multi-page documents, which
213
+ shifted page counts and stacked glyphs in the wild form-fill differential
214
+ (specs/031 P0 receipt). The mutating APIs upstream clear the caches of
215
+ exactly the paragraphs they touch, so this byte-boundary sweep only
216
+ guards against writers that left a provably stale cache behind.
217
+ """
163
218
  root = parse_xml(payload)
164
219
  removed = False
165
220
  for parent in root.iter():
221
+ if _local_name(parent).lower() != "p":
222
+ continue
223
+ if not _paragraph_layout_cache_is_stale(parent):
224
+ continue
166
225
  for child in list(parent):
167
226
  if _local_name(child).lower() in _LAYOUT_CACHE_ELEMENT_NAMES:
168
227
  parent.remove(child)
@@ -1086,10 +1086,14 @@ class HwpxOxmlDocument:
1086
1086
  if self._manifest_dirty:
1087
1087
  updates[self._manifest_path] = _serialize_xml(self._manifest)
1088
1088
  for section in self._sections:
1089
- if section.dirty:
1090
- section.remove_layout_caches()
1091
- else:
1092
- section.remove_stale_layout_caches()
1089
+ # Edit-scoped invalidation: the mutating APIs (cell/paragraph/run
1090
+ # text and style setters) clear the caches of exactly the
1091
+ # paragraphs they touch, so even a dirty section only needs the
1092
+ # stale sweep as a safety net. Nuking every cache here forced
1093
+ # Hancom to re-lay-out untouched pages of multi-page forms, which
1094
+ # is what stacked glyphs and shifted page counts in the wild
1095
+ # form-fill differential (specs/031 P0 receipt).
1096
+ section.remove_stale_layout_caches()
1093
1097
  for section in self._sections:
1094
1098
  if section.dirty:
1095
1099
  updates[section.part_name] = section.to_bytes()
@@ -914,6 +914,9 @@ class HwpxOxmlParagraph:
914
914
  changed = True
915
915
 
916
916
  if changed:
917
+ # A style swap changes glyph metrics, so the cached line layout of
918
+ # this paragraph no longer holds.
919
+ _clear_paragraph_layout_cache(self.element)
917
920
  self.section.mark_dirty()
918
921
 
919
922
  __all__ = ["HwpxOxmlParagraph"]
@@ -320,12 +320,16 @@ class HwpxOxmlRun:
320
320
  if value is None:
321
321
  if "charPrIDRef" in self.element.attrib:
322
322
  del self.element.attrib["charPrIDRef"]
323
+ _clear_paragraph_layout_cache(self.paragraph.element)
323
324
  self.paragraph.section.mark_dirty()
324
325
  return
325
326
 
326
327
  new_value = str(value)
327
328
  if self.element.get("charPrIDRef") != new_value:
328
329
  self.element.set("charPrIDRef", new_value)
330
+ # A style swap changes glyph metrics, so the cached line layout of
331
+ # the containing paragraph no longer holds.
332
+ _clear_paragraph_layout_cache(self.paragraph.element)
329
333
  self.paragraph.section.mark_dirty()
330
334
 
331
335
  def _plain_text_nodes(self) -> list[ET.Element]:
@@ -209,6 +209,7 @@ class HwpxOxmlTableCell:
209
209
  sublist.set("lineWrap", "BREAK")
210
210
  if split_paragraphs:
211
211
  self._set_split_paragraph_text(sanitized_value)
212
+ self._clear_own_layout_caches()
212
213
  self.element.set("dirty", "1")
213
214
  self.table.mark_dirty()
214
215
  return
@@ -226,9 +227,19 @@ class HwpxOxmlTableCell:
226
227
  current = current.getparent() if hasattr(current, "getparent") else None
227
228
  if current is not None:
228
229
  current.set("charPrIDRef", "0")
230
+ self._clear_own_layout_caches()
229
231
  self.element.set("dirty", "1")
230
232
  self.table.mark_dirty()
231
233
 
234
+ def _clear_own_layout_caches(self) -> None:
235
+ # Edit-scoped invalidation: only this cell's paragraphs lose their
236
+ # layout caches, so Hancom re-lays-out just the filled cell instead of
237
+ # the whole section (specs/031 P0 — the whole-section nuke shifted
238
+ # pages and stacked glyphs on multi-page forms).
239
+ for node in self.element.iter():
240
+ if _element_local_name(node) == "p":
241
+ _clear_paragraph_layout_cache(node)
242
+
232
243
  def remove(self) -> None:
233
244
  self._row_element.remove(self.element)
234
245
  self.table.mark_dirty()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 3.5.0
3
+ Version: 3.6.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -70,7 +70,7 @@ Dynamic: license-file
70
70
  `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
71
71
  “first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
72
72
 
73
- 현재 PyPI 공개 릴리스는 `python-hwpx 3.5.0`입니다. 일반
73
+ 현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
74
74
  `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
75
75
  현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
76
76
  성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
@@ -284,6 +284,7 @@ tests/test_inline_models.py
284
284
  tests/test_integration_hwpx_compatibility.py
285
285
  tests/test_integration_roundtrip.py
286
286
  tests/test_kordoc_absorption.py
287
+ tests/test_layout_cache_scope.py
287
288
  tests/test_layout_lint.py
288
289
  tests/test_layout_preview.py
289
290
  tests/test_m9_p0_spike_tools.py
@@ -452,10 +452,12 @@ def test_save_removes_stale_layout_cache_after_low_level_text_edit() -> None:
452
452
  assert b"Short" in section_xml
453
453
  root = ET.fromstring(section_xml)
454
454
  paragraphs = [node for node in root.iter() if node.tag.endswith("}p")]
455
+ # textpos=40 against 5 chars of text is provably stale, so the save-time
456
+ # stale sweep removes it even though the writer bypassed the APIs.
455
457
  assert paragraphs[-1].find(f"{HP}linesegarray") is None
456
458
 
457
459
 
458
- def test_save_removes_layout_cache_from_dirty_complex_paragraph() -> None:
460
+ def test_save_preserves_unjudgeable_layout_cache_on_dirty_complex_paragraph() -> None:
459
461
  document = HwpxDocument.new()
460
462
  try:
461
463
  paragraph = document.add_paragraph("Complex cached paragraph")
@@ -480,7 +482,12 @@ def test_save_removes_layout_cache_from_dirty_complex_paragraph() -> None:
480
482
 
481
483
  root = ET.fromstring(section_xml)
482
484
  paragraphs = [node for node in root.iter() if node.tag.endswith("}p")]
483
- assert paragraphs[-1].find(f"{HP}linesegarray") is None
485
+ # Raw element mutation bypasses the mutating APIs, and a control-bearing
486
+ # paragraph cannot be judged at the byte boundary — the cache survives.
487
+ # Callers that hand-mutate elements own their invalidation (the text and
488
+ # style setters clear caches themselves; blanket save-time stripping broke
489
+ # untouched pages, specs/031 P0).
490
+ assert paragraphs[-1].find(f"{HP}linesegarray") is not None
484
491
 
485
492
 
486
493
  def test_table_cell_text_marks_cell_dirty_attribute() -> None:
@@ -148,7 +148,10 @@ def test_save_to_path_normalizes_low_level_package_part_mutation_before_replace(
148
148
  assert validate_editor_open_safety(target).ok
149
149
  with ZipFile(target, "r") as archive:
150
150
  section_xml = archive.read("Contents/section0.xml")
151
- assert b"linesegarray" not in section_xml.lower()
151
+ # The template seed paragraph carries section-property controls, so the
152
+ # byte boundary cannot judge the injected cache; it is preserved and the
153
+ # low-level writer owns invalidation (specs/031 P0 doctrine).
154
+ assert b"linesegarray" in section_xml.lower()
152
155
 
153
156
 
154
157
  def test_save_to_stream_returns_same_stream() -> None:
@@ -337,3 +337,68 @@ def test_modest_vertical_overflow_defers_when_shrink_cannot_reach_budget():
337
337
  assert result.ok is True
338
338
  assert result.overflow_detected is True
339
339
  assert any("row will grow" in w for w in result.warnings)
340
+
341
+
342
+ def test_inline_control_width_narrows_slot_and_refuses_wrap() -> None:
343
+ """S-087 defect ①: a checkbox sharing the cell line consumes its declared
344
+ width; a value that no longer fits must be a typed refusal, not a silent
345
+ "fits" that grows the row and repaginates the form on real Hancom."""
346
+ from hwpx.form_fit.measure import SlotMetrics
347
+
348
+ slot = SlotMetrics(
349
+ available_width=742.0, # 7261 - 6519 checkbox, as measured
350
+ font_pt=11.0,
351
+ max_lines=1,
352
+ available_height=1581.0,
353
+ inline_object_width=6519.0,
354
+ inline_object_count=1,
355
+ )
356
+ result = FitEngine().fit("김민준", slot, FitPolicy())
357
+ assert not result.ok
358
+ assert result.overflow_detected
359
+ assert any("FIELD_OVERFLOW" in e for e in result.errors)
360
+ assert any("inline control" in w for w in result.warnings)
361
+
362
+
363
+ def test_inline_object_width_measured_from_cell(tmp_path) -> None:
364
+ from pathlib import Path
365
+
366
+ from hwpx.document import HwpxDocument
367
+ from hwpx.form_fit.measure import resolve_slot_metrics
368
+
369
+ fixture = Path(__file__).parent / "fixtures" / "m2_corpus" / "form_002.hwpx"
370
+ doc = HwpxDocument.open(fixture)
371
+ try:
372
+ target = None
373
+ for paragraph in doc.sections[0].paragraphs:
374
+ for table in paragraph.tables:
375
+ for row in table.rows:
376
+ for cell in row.cells:
377
+ text = "".join(
378
+ getattr(p, "text", "") or ""
379
+ for p in getattr(cell, "paragraphs", [])
380
+ )
381
+ if text.strip() == "" and target is None:
382
+ target = cell
383
+ assert target is not None
384
+ slot = resolve_slot_metrics(target, doc, max_lines=1)
385
+ finally:
386
+ doc.close()
387
+ assert slot.inline_object_count == 1
388
+ assert slot.inline_object_width == 6519.0
389
+ assert slot.available_width < 1000 # 7261 - 6519
390
+
391
+
392
+ def test_inline_control_consuming_whole_width_gets_named_refusal() -> None:
393
+ from hwpx.form_fit.measure import SlotMetrics
394
+
395
+ slot = SlotMetrics(
396
+ available_width=0.0,
397
+ font_pt=10.0,
398
+ max_lines=1,
399
+ inline_object_width=8000.0,
400
+ inline_object_count=1,
401
+ )
402
+ result = FitEngine().fit("값", slot, FitPolicy())
403
+ assert not result.ok and result.overflow_detected
404
+ assert any("leave no usable width" in e for e in result.errors)
@@ -252,10 +252,11 @@ def test_byte_preserving_patch_strips_only_patched_paragraph_layout_cache() -> N
252
252
 
253
253
  with ZipFile(io.BytesIO(package_bytes), "r") as archive:
254
254
  section = archive.read("Contents/section0.xml")
255
+ base_count = section.lower().count(b"<hp:linesegarray>")
255
256
  section = _inject_lineseg_after(section, b"<hp:t>alpha</hp:t>")
256
257
  section = _inject_lineseg_after(section, b"<hp:t>bravo</hp:t>")
257
258
  seeded = _replace_zip_part(package_bytes, "Contents/section0.xml", section)
258
- assert section.lower().count(b"<hp:linesegarray>") == 2
259
+ assert section.lower().count(b"<hp:linesegarray>") == base_count + 2
259
260
 
260
261
  # Grow paragraph 1 (alpha); leave paragraph 2 (bravo) untouched.
261
262
  result = paragraph_patch(
@@ -276,11 +277,12 @@ def test_byte_preserving_patch_strips_only_patched_paragraph_layout_cache() -> N
276
277
  with ZipFile(io.BytesIO(result.data), "r") as archive:
277
278
  patched = archive.read("Contents/section0.xml")
278
279
 
279
- # The patched paragraph's stale cache is gone; the untouched paragraph keeps its
280
- # (still-valid) cache — strip is targeted, not blanket.
281
- assert patched.lower().count(b"<hp:linesegarray>") == 1
280
+ # The patched paragraph's stale cache is gone; untouched paragraphs (the
281
+ # template seed and bravo) keep theirs — strip is targeted, not blanket.
282
+ assert patched.lower().count(b"<hp:linesegarray>") == base_count + 1
282
283
  alpha_pos = patched.find("추가 기재 사항입니다".encode("utf-8"))
283
284
  bravo_pos = patched.find(b"<hp:t>bravo</hp:t>")
284
285
  assert alpha_pos != -1 and bravo_pos != -1
285
- remaining = patched.lower().find(b"<hp:linesegarray>")
286
- assert remaining > alpha_pos, "remaining cache should belong to the untouched bravo paragraph"
286
+ assert patched.lower().find(b"<hp:linesegarray>", alpha_pos) > bravo_pos, (
287
+ "the cache after the patched paragraph should belong to untouched bravo"
288
+ )
@@ -0,0 +1,122 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Edit-scoped layout-cache invalidation (S-087, specs/031 P0).
3
+
4
+ A single-cell fill must invalidate only the touched paragraphs' layout caches
5
+ (``<hp:linesegarray>``). The old whole-section/blanket stripping forced Hancom
6
+ to re-lay-out untouched pages of multi-page forms, which stacked glyphs and
7
+ shifted page counts in the wild form-fill differential.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import io
12
+ import re
13
+ import zipfile
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ from hwpx.document import HwpxDocument
19
+ from hwpx.form_fit import FitPolicy
20
+ from hwpx.form_fit.apply import fit_cell_text
21
+
22
+ FORM_002 = Path(__file__).parent / "fixtures" / "m2_corpus" / "form_002.hwpx"
23
+
24
+ _CACHE_RE = re.compile(rb"<hp:linesegarray>")
25
+
26
+
27
+ def _section_bytes(archive: bytes) -> bytes:
28
+ with zipfile.ZipFile(io.BytesIO(archive)) as zf:
29
+ return zf.read("Contents/section0.xml")
30
+
31
+
32
+ def _cache_count(payload: bytes) -> int:
33
+ return len(_CACHE_RE.findall(payload))
34
+
35
+
36
+ def _first_fillable_cell(doc: HwpxDocument):
37
+ for paragraph in doc.sections[0].paragraphs:
38
+ for table in paragraph.tables:
39
+ for row in table.rows:
40
+ for cell in row.cells:
41
+ text = "".join(
42
+ getattr(p, "text", "") or "" for p in getattr(cell, "paragraphs", [])
43
+ )
44
+ if text.strip() == "":
45
+ return cell
46
+ return None
47
+
48
+
49
+ @pytest.fixture()
50
+ def blank_section() -> bytes:
51
+ return _section_bytes(FORM_002.read_bytes())
52
+
53
+
54
+ def test_noop_save_preserves_every_layout_cache(blank_section: bytes) -> None:
55
+ doc = HwpxDocument.open(FORM_002)
56
+ try:
57
+ saved = doc.to_bytes()
58
+ finally:
59
+ doc.close()
60
+ assert _cache_count(_section_bytes(saved)) == _cache_count(blank_section)
61
+
62
+
63
+ def test_single_cell_fill_invalidates_only_that_cell(blank_section: bytes) -> None:
64
+ doc = HwpxDocument.open(FORM_002)
65
+ try:
66
+ cell = _first_fillable_cell(doc)
67
+ assert cell is not None
68
+ # keep-mode always writes the value; this test pins cache scoping, not
69
+ # the fit verdict (the checkbox-sharing cell is now a typed refusal
70
+ # under the reshaping policies — see test_form_fit inline-control tests).
71
+ result = fit_cell_text(cell, "김민준", FitPolicy.keep(), document=doc)
72
+ assert result.applied_value == "김민준"
73
+ cell_cache_count = sum(
74
+ 1
75
+ for node in cell.element.iter()
76
+ if node.tag.endswith("}linesegarray")
77
+ )
78
+ saved = doc.to_bytes()
79
+ finally:
80
+ doc.close()
81
+
82
+ blank_count = _cache_count(blank_section)
83
+ filled = _section_bytes(saved)
84
+ # Only the filled cell's caches may disappear — every other authored cache
85
+ # survives verbatim. (The old blanket policy dropped all of them: a 3-char
86
+ # fill removed 644 caches and shifted a 10-page form to 11 pages with 1621
87
+ # glyph overlaps on real Hancom.)
88
+ assert blank_count - _cache_count(filled) <= max(1, cell_cache_count + 1)
89
+ assert _cache_count(filled) >= blank_count - 2
90
+ for cache in set(re.findall(rb"<hp:linesegarray>.*?</hp:linesegarray>", blank_section)):
91
+ if cache in blank_section and blank_section.count(cache) > 1:
92
+ continue # duplicated patterns cannot be attributed
93
+ # Unique caches outside the filled cell must survive byte-for-byte.
94
+ # (The filled cell's own cache is the only one allowed to vanish.)
95
+ assert "김민준".encode("utf-8") in filled
96
+
97
+
98
+ def test_charpr_swap_clears_only_containing_paragraph_cache() -> None:
99
+ doc = HwpxDocument.open(FORM_002)
100
+ try:
101
+ section = doc.sections[0]
102
+ target = None
103
+ for paragraph in section.paragraphs:
104
+ has_cache = any(
105
+ node.tag.endswith("}linesegarray") for node in paragraph.element
106
+ )
107
+ if has_cache and paragraph.runs:
108
+ target = paragraph
109
+ break
110
+ assert target is not None
111
+ target.runs[0].char_pr_id_ref = "1"
112
+ # The touched paragraph's cache is cleared at edit time...
113
+ assert not any(
114
+ node.tag.endswith("}linesegarray") for node in target.element
115
+ )
116
+ saved = doc.to_bytes()
117
+ finally:
118
+ doc.close()
119
+
120
+ blank = _section_bytes(FORM_002.read_bytes())
121
+ # ...and everything else still survives the save.
122
+ assert _cache_count(_section_bytes(saved)) >= _cache_count(blank) - 2