python-hwpx 3.0.0__tar.gz → 3.2.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 (350) hide show
  1. {python_hwpx-3.0.0/src/python_hwpx.egg-info → python_hwpx-3.2.0}/PKG-INFO +17 -5
  2. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/README.md +16 -4
  3. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/pyproject.toml +33 -7
  4. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/__init__.py +26 -0
  5. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/catalog.py +85 -0
  6. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/commands.py +173 -9
  7. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/document.py +36 -4
  8. python_hwpx-3.2.0/src/hwpx/agent/form_plan.py +1685 -0
  9. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/model.py +24 -2
  10. python_hwpx-3.2.0/src/hwpx/agent/story.py +207 -0
  11. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/document.py +22 -11
  12. python_hwpx-3.2.0/src/hwpx/oxml/_document_impl.py +150 -0
  13. python_hwpx-3.2.0/src/hwpx/oxml/_document_primitives.py +610 -0
  14. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/document.py +3 -1
  15. python_hwpx-3.2.0/src/hwpx/oxml/document_parts.py +1093 -0
  16. python_hwpx-3.2.0/src/hwpx/oxml/header_part.py +1329 -0
  17. python_hwpx-3.2.0/src/hwpx/oxml/memo.py +290 -0
  18. python_hwpx-3.2.0/src/hwpx/oxml/numbering.py +30 -0
  19. python_hwpx-3.2.0/src/hwpx/oxml/objects.py +470 -0
  20. python_hwpx-3.2.0/src/hwpx/oxml/paragraph.py +919 -0
  21. python_hwpx-3.2.0/src/hwpx/oxml/run.py +450 -0
  22. python_hwpx-3.2.0/src/hwpx/oxml/section.py +322 -0
  23. python_hwpx-3.2.0/src/hwpx/oxml/section_format.py +513 -0
  24. python_hwpx-3.2.0/src/hwpx/oxml/section_story.py +586 -0
  25. python_hwpx-3.2.0/src/hwpx/oxml/simple_parts.py +69 -0
  26. python_hwpx-3.2.0/src/hwpx/oxml/table.py +994 -0
  27. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/table_patch.py +125 -8
  28. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/package_validator.py +83 -16
  29. {python_hwpx-3.0.0 → python_hwpx-3.2.0/src/python_hwpx.egg-info}/PKG-INFO +17 -5
  30. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/python_hwpx.egg-info/SOURCES.txt +10 -0
  31. python_hwpx-3.2.0/tests/test_agent_mixed_form.py +726 -0
  32. python_hwpx-3.2.0/tests/test_agent_story_commands.py +602 -0
  33. python_hwpx-3.2.0/tests/test_mixed_form_plan.py +426 -0
  34. python_hwpx-3.2.0/tests/test_oxml_modularization.py +335 -0
  35. python_hwpx-3.2.0/tests/test_paragraph_section_management.py +419 -0
  36. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_section_headers.py +132 -0
  37. python_hwpx-3.0.0/src/hwpx/oxml/_document_impl.py +0 -6866
  38. python_hwpx-3.0.0/src/hwpx/oxml/header_part.py +0 -11
  39. python_hwpx-3.0.0/src/hwpx/oxml/memo.py +0 -11
  40. python_hwpx-3.0.0/src/hwpx/oxml/numbering.py +0 -8
  41. python_hwpx-3.0.0/src/hwpx/oxml/objects.py +0 -8
  42. python_hwpx-3.0.0/src/hwpx/oxml/paragraph.py +0 -11
  43. python_hwpx-3.0.0/src/hwpx/oxml/run.py +0 -8
  44. python_hwpx-3.0.0/src/hwpx/oxml/section.py +0 -11
  45. python_hwpx-3.0.0/src/hwpx/oxml/simple_parts.py +0 -8
  46. python_hwpx-3.0.0/src/hwpx/oxml/table.py +0 -11
  47. python_hwpx-3.0.0/tests/test_paragraph_section_management.py +0 -213
  48. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/LICENSE +0 -0
  49. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/NOTICE +0 -0
  50. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/setup.cfg +0 -0
  51. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/__init__.py +0 -0
  52. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/__init__.py +0 -0
  53. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/bundle.py +0 -0
  54. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/catalog.py +0 -0
  55. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/dump.py +0 -0
  56. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/mapping.py +0 -0
  57. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/model.py +0 -0
  58. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/native.py +0 -0
  59. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/blueprint/replay.py +0 -0
  60. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/cli.py +0 -0
  61. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/path.py +0 -0
  62. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/agent/query.py +0 -0
  63. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/authoring.py +0 -0
  64. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/benchmark/__init__.py +0 -0
  65. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/benchmark/blind_eval.py +0 -0
  66. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/body_patch.py +0 -0
  67. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/builder/__init__.py +0 -0
  68. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/builder/core.py +0 -0
  69. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/builder/report.py +0 -0
  70. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/__init__.py +0 -0
  71. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/badges.py +0 -0
  72. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  73. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  74. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  75. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  76. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/corpus.py +0 -0
  77. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/report.py +0 -0
  78. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  79. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/conformance/runner.py +0 -0
  80. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  81. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/__init__.py +0 -0
  82. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/_support.py +0 -0
  83. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/composer.py +0 -0
  84. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/harvest.py +0 -0
  85. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/plan.py +0 -0
  86. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profile.py +0 -0
  87. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  88. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  89. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  90. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  91. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  92. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  93. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
  94. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
  95. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
  96. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
  97. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  98. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  99. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  100. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  101. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  102. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  103. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  104. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  105. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  106. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  107. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  108. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  109. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  110. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/design/validator.py +0 -0
  111. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/evalplan_fill.py +0 -0
  112. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/exam/__init__.py +0 -0
  113. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/exam/compose.py +0 -0
  114. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/exam/ir.py +0 -0
  115. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/exam/measure.py +0 -0
  116. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/exam/parser.py +0 -0
  117. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/exam/profile.py +0 -0
  118. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/fill_residue.py +0 -0
  119. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fill.py +0 -0
  120. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/__init__.py +0 -0
  121. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/apply.py +0 -0
  122. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/engine.py +0 -0
  123. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/measure.py +0 -0
  124. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/policy.py +0 -0
  125. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/report.py +0 -0
  126. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/seal.py +0 -0
  127. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/form_fit/wordbox.py +0 -0
  128. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/formfill_quality.py +0 -0
  129. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/guidance_scan.py +0 -0
  130. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/ingest/__init__.py +0 -0
  131. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/ingest/base.py +0 -0
  132. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  133. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/layout/__init__.py +0 -0
  134. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/layout/lint.py +0 -0
  135. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/layout/report.py +0 -0
  136. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/opc/package.py +0 -0
  137. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/opc/relationships.py +0 -0
  138. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/opc/security.py +0 -0
  139. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/opc/xml_utils.py +0 -0
  140. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/__init__.py +0 -0
  141. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/body.py +0 -0
  142. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  143. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/common.py +0 -0
  144. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/header.py +0 -0
  145. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/namespaces.py +0 -0
  146. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/parser.py +0 -0
  147. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/schema.py +0 -0
  148. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/oxml/utils.py +0 -0
  149. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/package.py +0 -0
  150. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/patch.py +0 -0
  151. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/presets/__init__.py +0 -0
  152. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/presets/proposal.py +0 -0
  153. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/py.typed +0 -0
  154. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/quality/__init__.py +0 -0
  155. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/quality/ledger.py +0 -0
  156. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/quality/policy.py +0 -0
  157. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/quality/report.py +0 -0
  158. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/quality/save_pipeline.py +0 -0
  159. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/template_formfit.py +0 -0
  160. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/templates.py +0 -0
  161. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/__init__.py +0 -0
  162. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  163. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  164. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/advanced_generators.py +0 -0
  165. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/archive_cli.py +0 -0
  166. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/doc_diff.py +0 -0
  167. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/exporter.py +0 -0
  168. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  169. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  170. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  171. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  172. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  173. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  174. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/generic_inventory.py +0 -0
  175. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/id_integrity.py +0 -0
  176. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/idempotence.py +0 -0
  177. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/ir_equality.py +0 -0
  178. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/layout_preview.py +0 -0
  179. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/mail_merge.py +0 -0
  180. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/markdown_export.py +0 -0
  181. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/object_finder.py +0 -0
  182. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/official_lint.py +0 -0
  183. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/package_reconcile.py +0 -0
  184. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/page_guard.py +0 -0
  185. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/pii.py +0 -0
  186. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/read_fidelity.py +0 -0
  187. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/recover.py +0 -0
  188. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/redline.py +0 -0
  189. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/repair.py +0 -0
  190. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/report_parser.py +0 -0
  191. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/report_utils.py +0 -0
  192. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  193. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/style_profile.py +0 -0
  194. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/table_cleanup.py +0 -0
  195. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/table_compute.py +0 -0
  196. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/table_navigation.py +0 -0
  197. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/template_analyzer.py +0 -0
  198. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  199. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/text_extractor.py +0 -0
  200. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/toc_author.py +0 -0
  201. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  202. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/tools/validator.py +0 -0
  203. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/__init__.py +0 -0
  204. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
  205. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
  206. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  207. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  208. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/detectors.py +0 -0
  209. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/diff.py +0 -0
  210. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/fixture_corpus.py +0 -0
  211. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/hancom_worker.py +0 -0
  212. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/masks.py +0 -0
  213. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/oracle.py +0 -0
  214. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/page_qa.py +0 -0
  215. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/qa_contracts.py +0 -0
  216. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/qa_metrics.py +0 -0
  217. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/hwpx/visual/report.py +0 -0
  218. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  219. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  220. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  221. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  222. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_advanced_generators.py +0 -0
  223. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_blueprint_contracts.py +0 -0
  224. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_blueprint_dump.py +0 -0
  225. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_blueprint_replay.py +0 -0
  226. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_catalog.py +0 -0
  227. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_cli.py +0 -0
  228. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_commands.py +0 -0
  229. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_contracts.py +0 -0
  230. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_document.py +0 -0
  231. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_agent_path_query.py +0 -0
  232. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_authoring_profile_routing.py +0 -0
  233. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
  234. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_authoring_render_check.py +0 -0
  235. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_blind_eval_fixture.py +0 -0
  236. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_body_patch.py +0 -0
  237. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_builder_core.py +0 -0
  238. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_builder_plan_v2.py +0 -0
  239. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_builder_vertical_slice.py +0 -0
  240. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_cell_line_spacing.py +0 -0
  241. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_comment_node_robustness.py +0 -0
  242. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_conformance.py +0 -0
  243. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_corpus_read_fidelity.py +0 -0
  244. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_coverage_promotion.py +0 -0
  245. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_coverage_targets.py +0 -0
  246. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_design_builder.py +0 -0
  247. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_deviations_registry.py +0 -0
  248. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_doc_diff.py +0 -0
  249. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_document_context_manager.py +0 -0
  250. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_document_formatting.py +0 -0
  251. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_document_plan.py +0 -0
  252. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_document_plan_computed_fields.py +0 -0
  253. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_document_save_api.py +0 -0
  254. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_evalplan_fill.py +0 -0
  255. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_exam_compose.py +0 -0
  256. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_exam_compose_oracle.py +0 -0
  257. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_exam_fixtures.py +0 -0
  258. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_exam_ir.py +0 -0
  259. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_exam_measure.py +0 -0
  260. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_exam_parser.py +0 -0
  261. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_exam_profile.py +0 -0
  262. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_existing_document_format_editing.py +0 -0
  263. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_fill_residue.py +0 -0
  264. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_form_fields.py +0 -0
  265. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_form_fill_split_run.py +0 -0
  266. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_form_fit.py +0 -0
  267. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_form_fit_integration.py +0 -0
  268. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_form_fit_seal.py +0 -0
  269. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_form_fit_seal_placement.py +0 -0
  270. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_form_fit_wordbox.py +0 -0
  271. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_formfill_quality.py +0 -0
  272. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_fuzz_loop.py +0 -0
  273. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_fuzz_regressions.py +0 -0
  274. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_gap_closure_tools.py +0 -0
  275. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_government_report_preset.py +0 -0
  276. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_government_table_profile.py +0 -0
  277. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_guidance_scan.py +0 -0
  278. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_hancom_render_worker.py +0 -0
  279. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_hp_tab_support.py +0 -0
  280. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  281. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_id_generator_range.py +0 -0
  282. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_id_integrity.py +0 -0
  283. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_image_object_workflow.py +0 -0
  284. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_ingest.py +0 -0
  285. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_inline_models.py +0 -0
  286. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  287. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_integration_roundtrip.py +0 -0
  288. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_kordoc_absorption.py +0 -0
  289. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_layout_lint.py +0 -0
  290. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_layout_preview.py +0 -0
  291. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_mail_merge_fit.py +0 -0
  292. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_mail_merge_table_compute.py +0 -0
  293. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_mail_merge_xlsx.py +0 -0
  294. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_markdown_export.py +0 -0
  295. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_memo_and_style_editing.py +0 -0
  296. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_namespace_handling.py +0 -0
  297. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_new_features.py +0 -0
  298. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_official_document_style.py +0 -0
  299. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_official_lint_gongmun_gate.py +0 -0
  300. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_official_lint_tableaware.py +0 -0
  301. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_opc_package.py +0 -0
  302. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_open_safety_corpus.py +0 -0
  303. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_openrate.py +0 -0
  304. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_oxml_parsing.py +0 -0
  305. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_packaging_license_metadata.py +0 -0
  306. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_packaging_py_typed.py +0 -0
  307. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_paragraph_keep_together.py +0 -0
  308. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_pii.py +0 -0
  309. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_pii_structural.py +0 -0
  310. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_pii_wiring.py +0 -0
  311. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_proposal_preset.py +0 -0
  312. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_public_runtime_boundary.py +0 -0
  313. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_question_split_detector.py +0 -0
  314. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_read_fidelity.py +0 -0
  315. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_recover_broken_zip.py +0 -0
  316. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_redline_authoring.py +0 -0
  317. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_redline_verify.py +0 -0
  318. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_repair_repack.py +0 -0
  319. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_report_parser.py +0 -0
  320. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_report_utils.py +0 -0
  321. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_repr_snapshots.py +0 -0
  322. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_rhwp_t1_gates.py +0 -0
  323. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_rhwp_t2_verification.py +0 -0
  324. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_roundtrip_fidelity.py +0 -0
  325. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_row_heights.py +0 -0
  326. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_save_pipeline.py +0 -0
  327. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  328. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_set_paragraph_format_keep.py +0 -0
  329. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_skeleton_template_ids.py +0 -0
  330. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_split_cell.py +0 -0
  331. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_split_merged_cell.py +0 -0
  332. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_style_profile.py +0 -0
  333. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_table_cleanup.py +0 -0
  334. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_table_navigation.py +0 -0
  335. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_table_patch.py +0 -0
  336. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_table_patch_dryrun.py +0 -0
  337. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_table_patch_m105.py +0 -0
  338. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_tables_default_border.py +0 -0
  339. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_template_analyzer_enrichment.py +0 -0
  340. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_template_formfit.py +0 -0
  341. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_text_extractor_annotations.py +0 -0
  342. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_toc_author.py +0 -0
  343. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_toc_fidelity.py +0 -0
  344. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_validation_severity.py +0 -0
  345. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_validator_comment_nodes.py +0 -0
  346. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_version_metadata.py +0 -0
  347. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_visual_fixture_corpus.py +0 -0
  348. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_visual_oracle.py +0 -0
  349. {python_hwpx-3.0.0 → python_hwpx-3.2.0}/tests/test_visual_qa_contracts.py +0 -0
  350. {python_hwpx-3.0.0 → python_hwpx-3.2.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.0.0
3
+ Version: 3.2.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>
@@ -64,15 +64,27 @@ Dynamic: license-file
64
64
  |---|---|---|
65
65
  | 📦 라이브러리 | **[`python-hwpx`](https://github.com/airmang/python-hwpx)** | 순수 파이썬 HWPX 파싱·편집·생성 코어 |
66
66
  | 🔌 MCP 서버 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop, VS Code 등)에서 HWPX 조작 |
67
- | 🎯 에이전트 스킬 | [`hwpx-skill`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 공식 온보딩 스킬 |
67
+ | 🎯 에이전트 스킬 | [`hwpx-plugin`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 first-party 플러그인·스킬 번들 |
68
+
69
+ `python-hwpx`는 HWPX 파싱·편집·생성을 제공하는 코어 라이브러리이며,
70
+ `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
71
+ “first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
72
+
73
+ 현재 PyPI 공개 릴리스는 `python-hwpx 3.2.0`입니다. 일반
74
+ `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
75
+ 현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
76
+ 성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
68
77
 
69
78
  ---
70
79
 
71
80
  ## 왜 python-hwpx인가
72
81
 
73
- - **한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
82
+ - **코어 편집에 한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
74
83
  - **읽기부터 생성까지 한 코어** — 텍스트/서식 추출, 문단·표·양식 편집, 새 문서 생성, XSD 스키마 검증을 하나의 API로 처리합니다.
75
- - **에이전트·자동화 친화** — 같은 스택 위에서 `hwpx-mcp-server`와 공식 스킬이 직결됩니다.
84
+ - **에이전트·자동화 친화** — 같은 프로젝트가 유지보수하는 `hwpx-mcp-server`와 `hwpx-plugin`이 코어에 연결됩니다.
85
+
86
+ 문서 파싱·편집·생성은 순수 Python으로 수행할 수 있습니다. 다만 페이지 나눔, 표 넘침,
87
+ 글꼴 대체 등 최종 시각 품질을 확언하려면 필요에 따라 실제 한컴 렌더 오라클을 별도로 사용합니다.
76
88
 
77
89
  ## 빠른 시작
78
90
 
@@ -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>
@@ -19,15 +19,27 @@
19
19
  |---|---|---|
20
20
  | 📦 라이브러리 | **[`python-hwpx`](https://github.com/airmang/python-hwpx)** | 순수 파이썬 HWPX 파싱·편집·생성 코어 |
21
21
  | 🔌 MCP 서버 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop, VS Code 등)에서 HWPX 조작 |
22
- | 🎯 에이전트 스킬 | [`hwpx-skill`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 공식 온보딩 스킬 |
22
+ | 🎯 에이전트 스킬 | [`hwpx-plugin`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 first-party 플러그인·스킬 번들 |
23
+
24
+ `python-hwpx`는 HWPX 파싱·편집·생성을 제공하는 코어 라이브러리이며,
25
+ `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
26
+ “first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
27
+
28
+ 현재 PyPI 공개 릴리스는 `python-hwpx 3.2.0`입니다. 일반
29
+ `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
30
+ 현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
31
+ 성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
23
32
 
24
33
  ---
25
34
 
26
35
  ## 왜 python-hwpx인가
27
36
 
28
- - **한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
37
+ - **코어 편집에 한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
29
38
  - **읽기부터 생성까지 한 코어** — 텍스트/서식 추출, 문단·표·양식 편집, 새 문서 생성, XSD 스키마 검증을 하나의 API로 처리합니다.
30
- - **에이전트·자동화 친화** — 같은 스택 위에서 `hwpx-mcp-server`와 공식 스킬이 직결됩니다.
39
+ - **에이전트·자동화 친화** — 같은 프로젝트가 유지보수하는 `hwpx-mcp-server`와 `hwpx-plugin`이 코어에 연결됩니다.
40
+
41
+ 문서 파싱·편집·생성은 순수 Python으로 수행할 수 있습니다. 다만 페이지 나눔, 표 넘침,
42
+ 글꼴 대체 등 최종 시각 품질을 확언하려면 필요에 따라 실제 한컴 렌더 오라클을 별도로 사용합니다.
31
43
 
32
44
  ## 빠른 시작
33
45
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "3.0.0"
7
+ version = "3.2.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -109,11 +109,25 @@ python_version = "3.10"
109
109
  follow_imports = "skip"
110
110
  files = [
111
111
  "src/hwpx/document.py",
112
+ "src/hwpx/agent/commands.py",
113
+ "src/hwpx/agent/document.py",
114
+ "src/hwpx/agent/story.py",
115
+ "src/hwpx/oxml/_document_impl.py",
116
+ "src/hwpx/oxml/_document_primitives.py",
112
117
  "src/hwpx/oxml/document.py",
118
+ "src/hwpx/oxml/document_parts.py",
119
+ "src/hwpx/oxml/header_part.py",
120
+ "src/hwpx/oxml/memo.py",
121
+ "src/hwpx/oxml/numbering.py",
122
+ "src/hwpx/oxml/objects.py",
113
123
  "src/hwpx/opc/package.py",
114
124
  "src/hwpx/opc/relationships.py",
115
125
  "src/hwpx/oxml/paragraph.py",
116
126
  "src/hwpx/oxml/run.py",
127
+ "src/hwpx/oxml/section.py",
128
+ "src/hwpx/oxml/section_format.py",
129
+ "src/hwpx/oxml/section_story.py",
130
+ "src/hwpx/oxml/simple_parts.py",
117
131
  "src/hwpx/oxml/table.py",
118
132
  "src/hwpx/quality/policy.py",
119
133
  "src/hwpx/quality/report.py",
@@ -128,18 +142,30 @@ files = [
128
142
  ]
129
143
  ignore_missing_imports = true
130
144
 
131
- [[tool.mypy.overrides]]
132
- module = ["hwpx.document", "hwpx.oxml.document"]
133
- ignore_errors = true
134
-
135
145
  [tool.pyright]
136
- # Keep basic pyright focused on OPC/tooling modules that currently pass
137
- # without suppressing their real diagnostics.
146
+ # Keep basic pyright focused on the explicit gradual scope. Feature 025 adds
147
+ # both central facades and every extracted/touched runtime owner to the gate.
138
148
  include = [
149
+ "src/hwpx/document.py",
150
+ "src/hwpx/agent/commands.py",
151
+ "src/hwpx/agent/document.py",
152
+ "src/hwpx/agent/story.py",
153
+ "src/hwpx/oxml/_document_impl.py",
154
+ "src/hwpx/oxml/_document_primitives.py",
155
+ "src/hwpx/oxml/document.py",
156
+ "src/hwpx/oxml/document_parts.py",
157
+ "src/hwpx/oxml/header_part.py",
158
+ "src/hwpx/oxml/memo.py",
159
+ "src/hwpx/oxml/numbering.py",
160
+ "src/hwpx/oxml/objects.py",
139
161
  "src/hwpx/opc/package.py",
140
162
  "src/hwpx/opc/relationships.py",
141
163
  "src/hwpx/oxml/paragraph.py",
142
164
  "src/hwpx/oxml/run.py",
165
+ "src/hwpx/oxml/section.py",
166
+ "src/hwpx/oxml/section_format.py",
167
+ "src/hwpx/oxml/section_story.py",
168
+ "src/hwpx/oxml/simple_parts.py",
143
169
  "src/hwpx/oxml/table.py",
144
170
  "src/hwpx/quality/policy.py",
145
171
  "src/hwpx/quality/report.py",
@@ -18,6 +18,20 @@ from .model import (
18
18
  from .catalog import agent_catalog, agent_json_schemas, catalog_hash, human_help, node_help
19
19
  from .commands import apply_document_commands
20
20
  from .document import HwpxAgentDocument, NodeRecord
21
+ from .form_plan import (
22
+ MIXED_FORM_COMPILED_PLAN_SCHEMA,
23
+ MIXED_FORM_LOCATOR_KINDS,
24
+ MIXED_FORM_PLAN_SCHEMA,
25
+ MIXED_FORM_REQUEST_SCHEMA,
26
+ MixedFormPlan,
27
+ MixedFormResolution,
28
+ apply_mixed_form_fill,
29
+ apply_mixed_form_plan,
30
+ mixed_form_json_schemas,
31
+ plan_mixed_form_fill,
32
+ validate_mixed_form_plan,
33
+ validate_mixed_form_request,
34
+ )
21
35
  from .path import PathSegment, SemanticPath, canonicalize_path, parse_path
22
36
  from .query import QueryResult, SemanticSelector, parse_selector
23
37
  from .blueprint import (
@@ -52,6 +66,10 @@ __all__ = [
52
66
  "AGENT_COMMAND_SCHEMA",
53
67
  "AGENT_ERROR_SCHEMA",
54
68
  "AGENT_NODE_SCHEMA",
69
+ "MIXED_FORM_COMPILED_PLAN_SCHEMA",
70
+ "MIXED_FORM_LOCATOR_KINDS",
71
+ "MIXED_FORM_PLAN_SCHEMA",
72
+ "MIXED_FORM_REQUEST_SCHEMA",
55
73
  "BLUEPRINT_CATALOG_SCHEMA",
56
74
  "BLUEPRINT_REPLAY_RESULT_SCHEMA",
57
75
  "BLUEPRINT_REPLAY_SCHEMA",
@@ -65,6 +83,8 @@ __all__ = [
65
83
  "BlueprintBundle",
66
84
  "BlueprintDumpResult",
67
85
  "HwpxAgentDocument",
86
+ "MixedFormPlan",
87
+ "MixedFormResolution",
68
88
  "NodeRecord",
69
89
  "PathSegment",
70
90
  "QueryResult",
@@ -74,6 +94,8 @@ __all__ = [
74
94
  "agent_json_schemas",
75
95
  "agent_contract_manifest",
76
96
  "apply_document_commands",
97
+ "apply_mixed_form_fill",
98
+ "apply_mixed_form_plan",
77
99
  "blueprint_catalog",
78
100
  "blueprint_catalog_hash",
79
101
  "blueprint_hash",
@@ -84,15 +106,19 @@ __all__ = [
84
106
  "canonicalize_path",
85
107
  "catalog_hash",
86
108
  "human_help",
109
+ "mixed_form_json_schemas",
87
110
  "node_help",
88
111
  "parse_path",
89
112
  "parse_selector",
113
+ "plan_mixed_form_fill",
90
114
  "dump_document_blueprint",
91
115
  "read_blueprint_bundle",
92
116
  "repack_blueprint_bundle",
93
117
  "replay_document_blueprint",
94
118
  "validate_agent_batch",
95
119
  "validate_agent_command",
120
+ "validate_mixed_form_plan",
121
+ "validate_mixed_form_request",
96
122
  "validate_blueprint_manifest",
97
123
  "validate_replay_request",
98
124
  "write_blueprint_bundle",
@@ -9,10 +9,16 @@ from copy import deepcopy
9
9
  from typing import Any
10
10
 
11
11
  from .model import (
12
+ AGENT_BATCH_SCHEMA,
12
13
  AGENT_CATALOG_SCHEMA,
14
+ MAX_COMMANDS,
13
15
  AgentContractError,
14
16
  NODE_KINDS,
15
17
  NODE_PROPERTY_CATALOG_V1,
18
+ QUALITY_KEYS,
19
+ QUALITY_MODES,
20
+ REVISION_PATTERN,
21
+ VERIFICATION_REQUIREMENTS,
16
22
  agent_contract_manifest,
17
23
  )
18
24
 
@@ -78,6 +84,22 @@ def agent_json_schemas() -> dict[str, Any]:
78
84
  "properties": {key: value for key, value in properties.items() if key in allowed},
79
85
  }
80
86
  )
87
+ quality_object = {
88
+ "type": "object",
89
+ "additionalProperties": False,
90
+ "properties": {
91
+ "mode": {"enum": list(QUALITY_MODES)},
92
+ "renderCheck": {"enum": ["off", "auto", "required"]},
93
+ "xsdMode": {"enum": ["off", "lint"]},
94
+ "overflowPolicy": {"enum": ["fail", "warn", "truncate"]},
95
+ "layoutLint": {"enum": ["off", "warn", "strict"]},
96
+ "preserveUnmodifiedParts": {"type": "boolean"},
97
+ "requireReferenceIntegrity": {"type": "boolean"},
98
+ },
99
+ }
100
+ if set(quality_object["properties"]) != set(QUALITY_KEYS): # pragma: no cover
101
+ raise AssertionError("quality JSON Schema drifted from QUALITY_KEYS")
102
+
81
103
  return {
82
104
  "node": {
83
105
  "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -113,6 +135,69 @@ def agent_json_schemas() -> dict[str, Any]:
113
135
  "title": "HwpxAgentCommand v1",
114
136
  "oneOf": command_variants,
115
137
  },
138
+ "batch": {
139
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
140
+ "title": "HwpxAgentBatch v1",
141
+ "type": "object",
142
+ "required": [
143
+ "schemaVersion",
144
+ "input",
145
+ "output",
146
+ "commands",
147
+ "expectedRevision",
148
+ "idempotencyKey",
149
+ "dryRun",
150
+ "quality",
151
+ "verificationRequirements",
152
+ ],
153
+ "additionalProperties": False,
154
+ "properties": {
155
+ "schemaVersion": {"const": AGENT_BATCH_SCHEMA},
156
+ "input": {
157
+ "type": "object",
158
+ "required": ["filename"],
159
+ "additionalProperties": False,
160
+ "properties": {"filename": {"type": "string", "minLength": 1}},
161
+ },
162
+ "output": {
163
+ "type": "object",
164
+ "required": ["filename", "overwrite"],
165
+ "additionalProperties": False,
166
+ "properties": {
167
+ "filename": {"type": "string", "minLength": 1},
168
+ "overwrite": {"type": "boolean"},
169
+ },
170
+ },
171
+ "commands": {
172
+ "type": "array",
173
+ "minItems": 1,
174
+ "maxItems": MAX_COMMANDS,
175
+ "items": {"oneOf": deepcopy(command_variants)},
176
+ },
177
+ "expectedRevision": {
178
+ "type": ["string", "null"],
179
+ "pattern": REVISION_PATTERN.pattern,
180
+ },
181
+ "idempotencyKey": {
182
+ "type": ["string", "null"],
183
+ "minLength": 1,
184
+ "maxLength": 128,
185
+ },
186
+ "dryRun": {"type": "boolean"},
187
+ "quality": {
188
+ "oneOf": [
189
+ {"enum": list(QUALITY_MODES)},
190
+ deepcopy(quality_object),
191
+ {"type": "null"},
192
+ ]
193
+ },
194
+ "verificationRequirements": {
195
+ "type": "array",
196
+ "items": {"enum": list(VERIFICATION_REQUIREMENTS)},
197
+ "uniqueItems": True,
198
+ },
199
+ },
200
+ },
116
201
  "queryInput": {
117
202
  "$schema": "https://json-schema.org/draft/2020-12/schema",
118
203
  "type": "object",
@@ -21,7 +21,7 @@ from pathlib import Path
21
21
  from typing import Any
22
22
  from xml.etree import ElementTree as ET
23
23
 
24
- from lxml import etree as LET
24
+ from lxml import etree as LET # type: ignore[reportAttributeAccessIssue] # lxml has no complete bundled typing
25
25
 
26
26
  from hwpx.document import HwpxDocument
27
27
  from hwpx.oxml import HwpxOxmlTable
@@ -38,6 +38,12 @@ from .model import (
38
38
  validate_agent_batch,
39
39
  )
40
40
  from .path import parse_path
41
+ from .story import (
42
+ HEADER_STORY_EDITABLE_PROPERTIES,
43
+ HEADER_STORY_KIND,
44
+ HeaderStoryBinding,
45
+ try_parse_header_story_path,
46
+ )
41
47
 
42
48
  _EMPTY_REVISION = "sha256:" + hashlib.sha256(b"").hexdigest()
43
49
  _HWP_UNITS_PER_MM = 7200 / 25.4
@@ -242,6 +248,21 @@ def _validate_property_values(kind: str, properties: Mapping[str, Any], *, creat
242
248
  raise AgentContractError("unknown_property", f"untyped property: {target}", target=target)
243
249
 
244
250
 
251
+ def _validate_header_story_properties(properties: Mapping[str, Any]) -> None:
252
+ unknown = sorted(set(properties) - HEADER_STORY_EDITABLE_PROPERTIES)
253
+ if unknown:
254
+ raise AgentContractError(
255
+ "unknown_property",
256
+ f"header does not support properties: {unknown}",
257
+ target=f"header.{unknown[0]}",
258
+ )
259
+ if "text" not in properties:
260
+ raise AgentContractError(
261
+ "invalid_syntax", "existing header set requires text", target="header.text"
262
+ )
263
+ _require_string(properties["text"], "header.text")
264
+
265
+
245
266
  def _preflight(batch: Mapping[str, Any]) -> None:
246
267
  """Reject the complete static command matrix before the first mutation."""
247
268
 
@@ -254,6 +275,9 @@ def _preflight(batch: Mapping[str, Any]) -> None:
254
275
  return alias_kinds[command_id][field]
255
276
  except KeyError as exc: # validate_agent_batch already checks ordering
256
277
  raise AgentContractError("not_found", f"unknown command alias: {value}") from exc
278
+ story = try_parse_header_story_path(value)
279
+ if story is not None:
280
+ return story.kind
257
281
  parsed = parse_path(value)
258
282
  return parsed.segments[-1].kind if parsed.segments else "document"
259
283
 
@@ -264,6 +288,9 @@ def _preflight(batch: Mapping[str, Any]) -> None:
264
288
  return alias_kinds[command_id]["parentPath" if field == "path" else field]
265
289
  except KeyError:
266
290
  return "document"
291
+ story = try_parse_header_story_path(value)
292
+ if story is not None:
293
+ return "section"
267
294
  parsed = parse_path(value)
268
295
  return parsed.segments[-2].kind if len(parsed.segments) > 1 else "document"
269
296
 
@@ -298,6 +325,19 @@ def _preflight(batch: Mapping[str, Any]) -> None:
298
325
  }
299
326
  continue
300
327
  source_kind = reference_kind(command["path"])
328
+ if source_kind == HEADER_STORY_KIND:
329
+ if op != "set":
330
+ raise AgentContractError(
331
+ "unsupported_operation",
332
+ f"{op} is unsupported for {source_kind}",
333
+ target=command["commandId"],
334
+ )
335
+ _validate_header_story_properties(command["properties"])
336
+ alias_kinds[command["commandId"]] = {
337
+ "path": source_kind,
338
+ "parentPath": "section",
339
+ }
340
+ continue
301
341
  if op not in NODE_PROPERTY_CATALOG_V1[source_kind]["operations"]:
302
342
  raise AgentContractError(
303
343
  "unsupported_operation",
@@ -309,11 +349,11 @@ def _preflight(batch: Mapping[str, Any]) -> None:
309
349
  destination_kind = parent_kind(command["path"])
310
350
  if op in {"move", "copy"}:
311
351
  destination_kind = reference_kind(command["parent"])
312
- expected_parent = _MOVE_COPY_PARENTS.get(source_kind)
313
- if destination_kind != expected_parent:
352
+ move_parent = _MOVE_COPY_PARENTS.get(source_kind)
353
+ if destination_kind != move_parent:
314
354
  raise AgentContractError(
315
355
  "incompatible_parent",
316
- f"{source_kind} requires a {expected_parent} parent, not {destination_kind}",
356
+ f"{source_kind} requires a {move_parent} parent, not {destination_kind}",
317
357
  target=command["commandId"],
318
358
  )
319
359
  alias_kinds[command["commandId"]] = {
@@ -570,6 +610,42 @@ def _mark_containing_section(document: HwpxDocument, target: Any) -> None:
570
610
  raise AgentContractError("not_found", "mutated element is detached")
571
611
 
572
612
 
613
+ def _apply_header_story_set(
614
+ binding: HeaderStoryBinding, properties: Mapping[str, Any]
615
+ ) -> dict[str, Any]:
616
+ """Apply the bounded existing-header mutation through its OXML owner."""
617
+
618
+ _validate_header_story_properties(properties)
619
+ try:
620
+ binding.native.set_simple_text_preserving(properties["text"])
621
+ except AgentContractError:
622
+ raise
623
+ except ValueError as exc:
624
+ # The OXML owner uses ValueError for rich/control-bearing or otherwise
625
+ # structurally unsafe headers. Do not let it degrade to the generic
626
+ # invariant envelope or fall back to the destructive whole-story setter.
627
+ message = str(exc) or "header content cannot be edited losslessly"
628
+ lowered = message.lower()
629
+ code = (
630
+ "ambiguous_target"
631
+ if "ambiguous" in lowered
632
+ else "not_found"
633
+ if "not found" in lowered
634
+ else "unsupported_content"
635
+ )
636
+ raise AgentContractError(
637
+ code,
638
+ message,
639
+ target=binding.path,
640
+ ) from exc
641
+ return {
642
+ "text": {
643
+ "before": binding.text,
644
+ "after": properties["text"],
645
+ }
646
+ }
647
+
648
+
573
649
  def _apply_set(document: HwpxDocument, record: NodeRecord, properties: Mapping[str, Any]) -> dict[str, Any]:
574
650
  _ensure_operation(record, "set")
575
651
  _validate_property_values(record.kind, properties, creating=False)
@@ -711,7 +787,10 @@ def _add(
711
787
  if "pageHeightMm" in properties:
712
788
  size_kwargs["height"] = round(properties["pageHeightMm"] * _HWP_UNITS_PER_MM)
713
789
  if size_kwargs:
714
- created.properties.set_page_size(**size_kwargs)
790
+ created.properties.set_page_size(
791
+ width=size_kwargs.get("width"),
792
+ height=size_kwargs.get("height"),
793
+ )
715
794
  return created
716
795
  if kind == "paragraph":
717
796
  created = parent.native.add_paragraph(str(properties.get("text", "")))
@@ -1081,6 +1160,47 @@ def _member_diff(before: bytes, after: bytes) -> dict[str, Any]:
1081
1160
  return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
1082
1161
 
1083
1162
 
1163
+ def _verify_header_story_candidates(
1164
+ candidate_data: bytes,
1165
+ candidate_revision: str,
1166
+ expectations: Mapping[str, Mapping[str, str]],
1167
+ ) -> dict[str, Any]:
1168
+ receipts: list[dict[str, Any]] = []
1169
+ if expectations:
1170
+ with HwpxDocument.open(candidate_data) as reopened:
1171
+ view = HwpxAgentDocument.from_document(
1172
+ reopened, revision=candidate_revision
1173
+ )
1174
+ for expectation in expectations.values():
1175
+ path = expectation["path"]
1176
+ binding = view._resolve_header_story(path)
1177
+ if (
1178
+ binding.stable_id != expectation["stableId"]
1179
+ or binding.page_type != expectation["pageType"]
1180
+ or binding.text != expectation["text"]
1181
+ ):
1182
+ raise AgentContractError(
1183
+ "verification_failed",
1184
+ "reopened header story does not match the committed binding",
1185
+ target=path,
1186
+ )
1187
+ receipts.append(
1188
+ {
1189
+ "commandId": expectation["commandId"],
1190
+ "path": binding.path,
1191
+ "stableId": binding.stable_id,
1192
+ "pageType": binding.page_type,
1193
+ "textMatched": True,
1194
+ }
1195
+ )
1196
+ return {
1197
+ "schemaVersion": "hwpx.agent-story-preservation/v1",
1198
+ "ok": True,
1199
+ "storyCount": len(receipts),
1200
+ "stories": receipts,
1201
+ }
1202
+
1203
+
1084
1204
  def _request_hash(batch: Mapping[str, Any]) -> str:
1085
1205
  payload = json.dumps(
1086
1206
  batch,
@@ -1192,6 +1312,7 @@ def apply_document_commands(
1192
1312
  aliases: dict[str, dict[str, str]] = {}
1193
1313
  identity_changes: list[Mapping[str, str]] = []
1194
1314
  semantic_changes: list[Mapping[str, Any]] = []
1315
+ story_expectations: dict[str, Mapping[str, str]] = {}
1195
1316
  _call_fault(fault_injector, "before_open")
1196
1317
  with HwpxDocument.open(input_data) as document:
1197
1318
  view = HwpxAgentDocument.from_document(document, revision=input_revision)
@@ -1203,11 +1324,24 @@ def apply_document_commands(
1203
1324
  parent_path: str | None = None
1204
1325
  changed: dict[str, Any] = {}
1205
1326
  generated: list[dict[str, str]] = []
1327
+ target_native: Any | None = None
1328
+ story_before: HeaderStoryBinding | None = None
1206
1329
  if op == "set":
1207
1330
  resolved_path = _resolve_alias(command["path"], aliases)
1208
- record = view.resolve_record(resolved_path, expected_revision=input_revision)
1209
- changed = _apply_set(document, record, command["properties"])
1210
- target_native = record.native
1331
+ story_path = try_parse_header_story_path(resolved_path)
1332
+ if story_path is not None:
1333
+ story_before = view._resolve_header_story(
1334
+ story_path, expected_revision=input_revision
1335
+ )
1336
+ changed = _apply_header_story_set(
1337
+ story_before, command["properties"]
1338
+ )
1339
+ else:
1340
+ record = view.resolve_record(
1341
+ resolved_path, expected_revision=input_revision
1342
+ )
1343
+ changed = _apply_set(document, record, command["properties"])
1344
+ target_native = record.native
1211
1345
  elif op == "add":
1212
1346
  parent_path = _resolve_alias(command["parent"], aliases)
1213
1347
  parent = view.resolve_record(parent_path, expected_revision=input_revision)
@@ -1244,7 +1378,26 @@ def apply_document_commands(
1244
1378
 
1245
1379
  _call_fault(fault_injector, "after_command", index)
1246
1380
  view = HwpxAgentDocument.from_document(document, revision=input_revision)
1247
- if op == "set" and resolved_path is not None:
1381
+ result_stable_id: str | None = None
1382
+ if story_before is not None and resolved_path is not None:
1383
+ target_story = view._resolve_header_story(resolved_path)
1384
+ if target_story.stable_id != story_before.stable_id:
1385
+ raise AgentContractError(
1386
+ "invariant_violation",
1387
+ "header story identity changed during text edit",
1388
+ target=resolved_path,
1389
+ )
1390
+ result_path = target_story.path
1391
+ result_parent = target_story.parent_path
1392
+ result_stable_id = target_story.stable_id
1393
+ story_expectations[target_story.binding_key] = {
1394
+ "commandId": command_id,
1395
+ "path": target_story.path,
1396
+ "stableId": target_story.stable_id,
1397
+ "pageType": target_story.page_type,
1398
+ "text": command["properties"]["text"],
1399
+ }
1400
+ elif op == "set" and resolved_path is not None:
1248
1401
  # Some native bindings (notably form fields) are request-local
1249
1402
  # match dictionaries. A property edit is non-structural, so
1250
1403
  # its canonical path is the durable post-edit lookup key.
@@ -1269,6 +1422,11 @@ def apply_document_commands(
1269
1422
  "generatedIdentities": generated,
1270
1423
  "warnings": [],
1271
1424
  }
1425
+ if result_stable_id is not None:
1426
+ # Command results are already an untyped JSON mapping. This
1427
+ # story-only receipt carries the actual stable identity
1428
+ # without changing AgentBatchResult or the ToolSpec schema.
1429
+ result["stableId"] = result_stable_id
1272
1430
  command_results.append(result)
1273
1431
  semantic_changes.append(
1274
1432
  {
@@ -1285,6 +1443,12 @@ def apply_document_commands(
1285
1443
  _call_fault(fault_injector, "after_serialize")
1286
1444
 
1287
1445
  candidate_revision = _revision(candidate_data)
1446
+ if story_expectations:
1447
+ verification["storyPreservation"] = _verify_header_story_candidates(
1448
+ candidate_data,
1449
+ candidate_revision,
1450
+ story_expectations,
1451
+ )
1288
1452
  semantic_diff = {
1289
1453
  "schemaVersion": "hwpx.agent-semantic-diff/v1",
1290
1454
  "inputRevision": input_revision,