python-hwpx 5.5.0__tar.gz → 5.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. {python_hwpx-5.5.0/src/python_hwpx.egg-info → python_hwpx-5.7.0}/PKG-INFO +1 -1
  2. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/pyproject.toml +15 -2
  3. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/__init__.py +12 -0
  4. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/fields.py +115 -0
  5. python_hwpx-5.7.0/src/hwpx/capabilities.py +466 -0
  6. python_hwpx-5.7.0/src/hwpx/data/contract_docs/known-traps.md +68 -0
  7. python_hwpx-5.7.0/src/hwpx/data/contract_docs/mutation-semantics.md +59 -0
  8. python_hwpx-5.7.0/src/hwpx/data/contract_docs/recipes-traversal.md +115 -0
  9. python_hwpx-5.7.0/src/hwpx/data/contract_docs/support-matrix.md +61 -0
  10. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/document.py +62 -0
  11. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/experimental.py +20 -0
  12. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/paragraph.py +85 -0
  13. python_hwpx-5.7.0/src/hwpx/plan/__init__.py +48 -0
  14. python_hwpx-5.7.0/src/hwpx/plan/_execute.py +282 -0
  15. python_hwpx-5.7.0/src/hwpx/plan/_model.py +269 -0
  16. python_hwpx-5.7.0/src/hwpx/plan/_schema.py +127 -0
  17. python_hwpx-5.7.0/src/hwpx/plan/_validate.py +174 -0
  18. {python_hwpx-5.5.0 → python_hwpx-5.7.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  19. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/python_hwpx.egg-info/SOURCES.txt +14 -0
  20. python_hwpx-5.7.0/tests/test_capabilities_surface.py +137 -0
  21. python_hwpx-5.7.0/tests/test_check_box_authoring.py +154 -0
  22. python_hwpx-5.7.0/tests/test_contract_docs_sync.py +41 -0
  23. python_hwpx-5.7.0/tests/test_edit_plan.py +356 -0
  24. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_oxml_modularization.py +5 -4
  25. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_product_boundary.py +2 -1
  26. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_public_artifact_hygiene.py +1 -0
  27. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_stable_surface.py +3 -2
  28. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/LICENSE +0 -0
  29. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/NOTICE +0 -0
  30. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/README.md +0 -0
  31. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/setup.cfg +0 -0
  32. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/__init__.py +0 -0
  33. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/_units.py +0 -0
  34. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/layout.py +0 -0
  35. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/media.py +0 -0
  36. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/memos.py +0 -0
  37. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/persistence.py +0 -0
  38. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/shapes.py +0 -0
  39. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/_document/tracked.py +0 -0
  40. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/body_patch.py +0 -0
  41. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  42. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/equation/__init__.py +0 -0
  43. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/equation/authoring.py +0 -0
  44. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/equation/eqedit.py +0 -0
  45. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/equation/mathml.py +0 -0
  46. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/equation/render.py +0 -0
  47. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/equation/tokens.py +0 -0
  48. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/errors.py +0 -0
  49. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/form_fit/__init__.py +0 -0
  50. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/form_fit/apply.py +0 -0
  51. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/form_fit/engine.py +0 -0
  52. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/form_fit/measure.py +0 -0
  53. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/form_fit/policy.py +0 -0
  54. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/form_fit/report.py +0 -0
  55. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/ingest/__init__.py +0 -0
  56. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/ingest/base.py +0 -0
  57. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  58. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/layout/__init__.py +0 -0
  59. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/layout/lint.py +0 -0
  60. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/layout/report.py +0 -0
  61. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/mutation_report.py +0 -0
  62. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/opc/package.py +0 -0
  63. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/opc/relationships.py +0 -0
  64. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/opc/security.py +0 -0
  65. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/opc/xml_utils.py +0 -0
  66. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/__init__.py +0 -0
  67. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/_document_impl.py +0 -0
  68. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/_document_primitives.py +0 -0
  69. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/body.py +0 -0
  70. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  71. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/color.py +0 -0
  72. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/common.py +0 -0
  73. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/document.py +0 -0
  74. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/document_parts.py +0 -0
  75. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/header.py +0 -0
  76. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/header_part.py +0 -0
  77. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/memo.py +0 -0
  78. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/namespaces.py +0 -0
  79. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/numbering.py +0 -0
  80. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/objects.py +0 -0
  81. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/parser.py +0 -0
  82. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/run.py +0 -0
  83. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/schema.py +0 -0
  84. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/section.py +0 -0
  85. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/section_format.py +0 -0
  86. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/section_story.py +0 -0
  87. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/simple_parts.py +0 -0
  88. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/table.py +0 -0
  89. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/oxml/utils.py +0 -0
  90. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/package.py +0 -0
  91. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/patch.py +0 -0
  92. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/py.typed +0 -0
  93. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/quality/__init__.py +0 -0
  94. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/quality/ledger.py +0 -0
  95. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/quality/policy.py +0 -0
  96. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/quality/rendering.py +0 -0
  97. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/quality/report.py +0 -0
  98. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/quality/save_pipeline.py +0 -0
  99. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/table_patch.py +0 -0
  100. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/templates.py +0 -0
  101. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/__init__.py +0 -0
  102. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  103. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  104. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/archive_cli.py +0 -0
  105. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/doc_diff.py +0 -0
  106. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/document_viewer.py +0 -0
  107. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/exporter.py +0 -0
  108. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/generic_inventory.py +0 -0
  109. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/id_integrity.py +0 -0
  110. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/idempotence.py +0 -0
  111. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/ir_equality.py +0 -0
  112. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/layout_preview.py +0 -0
  113. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/mail_merge.py +0 -0
  114. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/markdown_export.py +0 -0
  115. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/object_finder.py +0 -0
  116. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/package_reconcile.py +0 -0
  117. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/package_validator.py +0 -0
  118. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/page_guard.py +0 -0
  119. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/read_fidelity.py +0 -0
  120. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/recover.py +0 -0
  121. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/redline.py +0 -0
  122. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/repair.py +0 -0
  123. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/report_utils.py +0 -0
  124. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  125. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/table_cleanup.py +0 -0
  126. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/table_navigation.py +0 -0
  127. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/template_analyzer.py +0 -0
  128. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  129. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/text_extractor.py +0 -0
  130. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/toc_author.py +0 -0
  131. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  132. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/hwpx/tools/validator.py +0 -0
  133. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  134. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  135. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  136. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  137. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_add_control.py +0 -0
  138. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_adoption_metrics_snapshot.py +0 -0
  139. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_authoring_defaults.py +0 -0
  140. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_authoring_quality_corpus.py +0 -0
  141. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_blind_eval_fixture.py +0 -0
  142. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_byte_patch_identity.py +0 -0
  143. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_cell_line_spacing.py +0 -0
  144. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_chart_authoring.py +0 -0
  145. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_comment_node_robustness.py +0 -0
  146. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_corpus_read_fidelity.py +0 -0
  147. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_coverage_promotion.py +0 -0
  148. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_coverage_targets.py +0 -0
  149. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_deviations_registry.py +0 -0
  150. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_doc_diff.py +0 -0
  151. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_document_context_manager.py +0 -0
  152. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_document_facade_surface.py +0 -0
  153. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_document_formatting.py +0 -0
  154. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_document_ops_runtime_boundary.py +0 -0
  155. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_document_save_api.py +0 -0
  156. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_document_viewer.py +0 -0
  157. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_documentation_code_fences.py +0 -0
  158. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_equation_authoring.py +0 -0
  159. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_equation_converter.py +0 -0
  160. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_equation_render.py +0 -0
  161. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_error_messages.py +0 -0
  162. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_evalplan_core_primitives.py +0 -0
  163. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_existing_document_format_editing.py +0 -0
  164. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_form_field_authoring.py +0 -0
  165. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_form_fields.py +0 -0
  166. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_form_fill_core_primitives.py +0 -0
  167. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_form_fit.py +0 -0
  168. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_form_fit_integration.py +0 -0
  169. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
  170. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_fuzz_loop.py +0 -0
  171. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_fuzz_regressions.py +0 -0
  172. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_gap_closure_tools.py +0 -0
  173. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_golden_api.py +0 -0
  174. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_hp_tab_support.py +0 -0
  175. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  176. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_id_generator_range.py +0 -0
  177. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_image_object_workflow.py +0 -0
  178. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_ingest.py +0 -0
  179. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_inline_models.py +0 -0
  180. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  181. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_integration_roundtrip.py +0 -0
  182. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_layout_cache_scope.py +0 -0
  183. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_layout_lint.py +0 -0
  184. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_layout_preview.py +0 -0
  185. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_library_boundary_closure.py +0 -0
  186. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_llms_txt.py +0 -0
  187. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_mail_merge_xlsx.py +0 -0
  188. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_markdown_export.py +0 -0
  189. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_memo_and_style_editing.py +0 -0
  190. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_moved_surface_hints.py +0 -0
  191. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_mutation_report.py +0 -0
  192. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_namespace_handling.py +0 -0
  193. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_new_features.py +0 -0
  194. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_note_authoring_contract.py +0 -0
  195. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_opc_package.py +0 -0
  196. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_open_safety_corpus.py +0 -0
  197. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_oxml_parsing.py +0 -0
  198. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_packaging_license_metadata.py +0 -0
  199. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_packaging_py_typed.py +0 -0
  200. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_paragraph_keep_together.py +0 -0
  201. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_paragraph_section_management.py +0 -0
  202. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_public_runtime_boundary.py +0 -0
  203. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_read_fidelity.py +0 -0
  204. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_recover_broken_zip.py +0 -0
  205. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_redline_authoring.py +0 -0
  206. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_redline_verify.py +0 -0
  207. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_release_workflow_safety.py +0 -0
  208. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_repair_repack.py +0 -0
  209. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_report_utils.py +0 -0
  210. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_repr_snapshots.py +0 -0
  211. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_roundtrip_fidelity.py +0 -0
  212. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_row_heights.py +0 -0
  213. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_save_pipeline.py +0 -0
  214. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_section_headers.py +0 -0
  215. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_set_paragraph_format_keep.py +0 -0
  216. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_shape_geometry_contract.py +0 -0
  217. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_skeleton_template_ids.py +0 -0
  218. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_split_cell.py +0 -0
  219. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_split_merged_cell.py +0 -0
  220. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_table_cleanup.py +0 -0
  221. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_table_navigation.py +0 -0
  222. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_table_patch.py +0 -0
  223. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_table_patch_dryrun.py +0 -0
  224. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_table_patch_m105.py +0 -0
  225. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_tables_default_border.py +0 -0
  226. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_template_analyzer_enrichment.py +0 -0
  227. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_text_extractor_annotations.py +0 -0
  228. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_toc_author.py +0 -0
  229. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_toc_fidelity.py +0 -0
  230. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_validation_severity.py +0 -0
  231. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_validator_comment_nodes.py +0 -0
  232. {python_hwpx-5.5.0 → python_hwpx-5.7.0}/tests/test_version_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 5.5.0
3
+ Version: 5.7.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "5.5.0"
7
+ version = "5.7.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -109,6 +109,7 @@ packages = [
109
109
  "hwpx.layout",
110
110
  "hwpx.opc",
111
111
  "hwpx.oxml",
112
+ "hwpx.plan",
112
113
  "hwpx.quality",
113
114
  "hwpx.tools",
114
115
  "hwpx.tools._schemas",
@@ -125,7 +126,7 @@ packages = [
125
126
  [tool.setuptools.package-data]
126
127
  "hwpx" = ["py.typed"]
127
128
  "hwpx.tools" = ["_schemas/*.xsd"]
128
- "hwpx.data" = ["Skeleton.hwpx"]
129
+ "hwpx.data" = ["Skeleton.hwpx", "contract_docs/*.md"]
129
130
 
130
131
  [tool.pytest.ini_options]
131
132
  # scripts/ carries repository QA harnesses that are not library surface — the
@@ -145,6 +146,12 @@ files = [
145
146
  "src/hwpx/document.py",
146
147
  "src/hwpx/errors.py",
147
148
  "src/hwpx/mutation_report.py",
149
+ "src/hwpx/capabilities.py",
150
+ "src/hwpx/plan/__init__.py",
151
+ "src/hwpx/plan/_model.py",
152
+ "src/hwpx/plan/_validate.py",
153
+ "src/hwpx/plan/_execute.py",
154
+ "src/hwpx/plan/_schema.py",
148
155
  "src/hwpx/equation/__init__.py",
149
156
  "src/hwpx/equation/tokens.py",
150
157
  "src/hwpx/equation/eqedit.py",
@@ -200,6 +207,12 @@ include = [
200
207
  "src/hwpx/document.py",
201
208
  "src/hwpx/errors.py",
202
209
  "src/hwpx/mutation_report.py",
210
+ "src/hwpx/capabilities.py",
211
+ "src/hwpx/plan/__init__.py",
212
+ "src/hwpx/plan/_model.py",
213
+ "src/hwpx/plan/_validate.py",
214
+ "src/hwpx/plan/_execute.py",
215
+ "src/hwpx/plan/_schema.py",
203
216
  "src/hwpx/equation/__init__.py",
204
217
  "src/hwpx/equation/tokens.py",
205
218
  "src/hwpx/equation/eqedit.py",
@@ -52,6 +52,18 @@ _EXPERIMENTAL_EXPORTS = {
52
52
  "UnsupportedLatexError": "hwpx.equation.authoring",
53
53
  "estimate_equation_size": "hwpx.equation.authoring",
54
54
  "latex_to_eqedit": "hwpx.equation.authoring",
55
+ # 편집 계획 실행기(5.6.0 신규, 선언적 다단 편집 -> 원자 실행). 계약 유동.
56
+ "EditPlan": "hwpx.plan",
57
+ "PlanReport": "hwpx.plan",
58
+ "PlanValidationError": "hwpx.plan",
59
+ "apply_edit_plan": "hwpx.plan",
60
+ "validate_edit_plan": "hwpx.plan",
61
+ # 기계가독 자기서술(5.6.0 신규, 드리프트 가드 포함). 계약 유동.
62
+ # 함수명은 describe_capabilities — 모듈 hwpx.capabilities와 이름이 같으면
63
+ # 서브모듈 바인딩이 lazy 알리아스를 그림자로 가려 경고 계약이 깨진다.
64
+ "describe_capabilities": "hwpx.capabilities",
65
+ "contract_document": "hwpx.capabilities",
66
+ "contract_json_schema": "hwpx.capabilities",
55
67
  }
56
68
 
57
69
  # Emptied in 5.0. The 4.x notice on these names said they would go in the next
@@ -386,6 +386,121 @@ def add_form_field(
386
386
  )
387
387
 
388
388
 
389
+ _CHECK_BOX_TAG = f"{_HP}checkBtn"
390
+
391
+
392
+ def _iter_check_boxes(doc: "HwpxDocument") -> list[dict[str, Any]]:
393
+ """Every ``<hp:checkBtn>`` in document order, with its owning element."""
394
+
395
+ found: list[dict[str, Any]] = []
396
+ index = 0
397
+ for section_index, section in enumerate(doc.sections):
398
+ for element in section.element.iter(_CHECK_BOX_TAG):
399
+ found.append(
400
+ {
401
+ "index": index,
402
+ "sectionIndex": section_index,
403
+ "name": element.get("name", ""),
404
+ "caption": element.get("caption", ""),
405
+ "checked": element.get("value", "").upper() == "CHECKED",
406
+ "value": element.get("value", ""),
407
+ "_element": element,
408
+ "_section": section,
409
+ }
410
+ )
411
+ index += 1
412
+ return found
413
+
414
+
415
+ def list_check_boxes(doc: "HwpxDocument") -> list[dict[str, Any]]:
416
+ """Return check-box form objects in document order."""
417
+
418
+ return [
419
+ {key: value for key, value in match.items() if not key.startswith("_")}
420
+ for match in _iter_check_boxes(doc)
421
+ ]
422
+
423
+
424
+ def add_check_box(
425
+ doc: "HwpxDocument",
426
+ caption: str,
427
+ *,
428
+ checked: bool = False,
429
+ name: str | None = None,
430
+ paragraph: Any | None = None,
431
+ section: Any | None = None,
432
+ section_index: int | None = None,
433
+ ) -> dict[str, Any]:
434
+ """Create a check-box form object and return its payload.
435
+
436
+ The emitted XML follows the check-box contract measured against real
437
+ Hancom (specs/060): ``value`` selects ☑/□ and the ``<hp:formCharPr>`` child
438
+ is mandatory — without it Hancom refuses the document. The created object is
439
+ re-read through the standard :func:`list_check_boxes` reader, so creation
440
+ fails loudly if the ordinary consumer would not see it.
441
+ """
442
+
443
+ if not str(caption).strip():
444
+ raise ValueError("check box caption must be a non-empty string")
445
+ if paragraph is None:
446
+ paragraph = doc.add_paragraph(
447
+ "", section=section, section_index=section_index, include_run=False,
448
+ )
449
+
450
+ control = paragraph.add_check_box(caption, checked=checked, name=name)
451
+ _clear_form_field_layout_cache(paragraph.element)
452
+
453
+ created_name = control.element.get("name", "")
454
+ for match in _iter_check_boxes(doc):
455
+ if match["_element"] is control.element:
456
+ return {
457
+ key: value for key, value in match.items() if not key.startswith("_")
458
+ }
459
+ raise RuntimeError(
460
+ f"created check box {created_name!r} was not recognized by the standard reader"
461
+ )
462
+
463
+
464
+ def set_check_box(
465
+ doc: "HwpxDocument",
466
+ checked: bool,
467
+ *,
468
+ index: int | None = None,
469
+ name: str | None = None,
470
+ ) -> dict[str, Any]:
471
+ """Set a check box's state, selecting it by ``index`` or ``name``.
472
+
473
+ Exactly one selector is required — an ambiguous or missing selector is a
474
+ typed refusal, never a guess.
475
+ """
476
+
477
+ if (index is None) == (name is None):
478
+ raise ValueError("provide exactly one of index or name")
479
+ matches = _iter_check_boxes(doc)
480
+ if index is not None:
481
+ chosen = [m for m in matches if m["index"] == index]
482
+ if not chosen:
483
+ raise ValueError(f"check box index not found: {index}")
484
+ else:
485
+ wanted = str(name).strip()
486
+ chosen = [m for m in matches if m["name"] == wanted]
487
+ if not chosen:
488
+ raise ValueError(f"check box name not found: {wanted}")
489
+ if len(chosen) > 1:
490
+ raise ValueError(
491
+ f"check box name is ambiguous ({len(chosen)} matches): {wanted}"
492
+ )
493
+ match = chosen[0]
494
+ match["_element"].set("value", "CHECKED" if checked else "UNCHECKED")
495
+ match["_section"].mark_dirty()
496
+ return {
497
+ key: value
498
+ for key, value in {**match, "checked": bool(checked),
499
+ "value": "CHECKED" if checked else "UNCHECKED"}.items()
500
+ if not key.startswith("_")
501
+ }
502
+
503
+
389
504
  def _select_form_field(
390
505
  doc: "HwpxDocument",
391
506
  matches: Sequence[dict[str, Any]],
@@ -0,0 +1,466 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """``hwpx.capabilities`` — 기계가독 자기서술 (experimental, 5.6.0 신규).
3
+
4
+ 동기(실측 드리프트 사례): 사람 손으로 갱신하는 자기서술 문서는 릴리스 트레인
5
+ 하루 만에도 실표면과 드리프트한다. 이 모듈이 단일 진실 원천이고, 드리프트
6
+ 가드 테스트가 다음 세 방향을 강제한다:
7
+
8
+ 1. 여기 등재된 진입점은 전부 실제로 import 가능해야 한다.
9
+ 2. ``editPlanOps``는 :data:`hwpx.plan.PLAN_OPS`·실행 디스패치·edit-plan JSON
10
+ Schema enum과 일치해야 한다.
11
+ 3. :data:`_CAPABILITY_AREAS`의 매트릭스 행 제목은 ``docs/support-matrix.md``
12
+ 표의 행 제목과 집합 일치해야 한다.
13
+
14
+ **core는 환경 변수를 읽지 않는다** — 렌더 오라클 가용성은 여기 없다(자기서술은
15
+ ``renderOracle.bundled="none"``으로 그 사실 자체를 말한다). 오라클 탐지·실행은
16
+ automation 계층(python-hwpx-automation)이 보고한다.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import platform
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from .errors import HwpxError
26
+
27
+ CAPABILITIES_SCHEMA = "hwpx.capabilities/v1"
28
+
29
+ _DIST_NAME = "python-hwpx"
30
+
31
+ #: extras 이름 → 그 extra가 끄는 런타임 모듈들. ``visual``은 5.0부터 의도적으로
32
+ #: 빈 extra(설치 호환 유지용)라 백킹 모듈이 없고, 프로브는 공허하게 참이다.
33
+ _EXTRA_MODULES: dict[str, tuple[str, ...]] = {
34
+ "visual": (),
35
+ "xlsx": ("openpyxl",),
36
+ "preview": ("latex2mathml",),
37
+ }
38
+
39
+ #: 능력 영역 레지스트리. ``matrix_row``는 ``docs/support-matrix.md`` 표 1열
40
+ #: 제목과 **정확히** 일치해야 한다(가드 테스트가 집합 대조). ``entry_points``는
41
+ #: ``"모듈:이름"`` — 전부 import 가능해야 한다(가드 테스트가 해석).
42
+ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
43
+ {
44
+ "area": "paragraph-table-authoring",
45
+ "matrix_row": "문단·표 저작/편집",
46
+ "entry_points": ("hwpx.document:HwpxDocument",),
47
+ },
48
+ {
49
+ "area": "table-structure",
50
+ "matrix_row": "표 구조 변경(행·열·표 삭제/삽입, 열 오토핏)",
51
+ "entry_points": ("hwpx.table_patch:apply_table_ops",),
52
+ },
53
+ {
54
+ "area": "table-create",
55
+ "matrix_row": "표 생성(병합·중첩 포함)",
56
+ "entry_points": ("hwpx.document:HwpxDocument",),
57
+ },
58
+ {
59
+ "area": "form-fill",
60
+ "matrix_row": "양식 채움(byte-splice)",
61
+ "entry_points": (
62
+ "hwpx.patch:paragraph_patch",
63
+ "hwpx.table_patch:fill_cells",
64
+ "hwpx.body_patch:apply_body_ops",
65
+ ),
66
+ },
67
+ {
68
+ "area": "edit-plan",
69
+ "matrix_row": "편집 계획 실행(edit plan)",
70
+ "entry_points": (
71
+ "hwpx.plan:apply_edit_plan",
72
+ "hwpx.plan:validate_edit_plan",
73
+ ),
74
+ },
75
+ {
76
+ "area": "shape-authoring",
77
+ "matrix_row": "도형 저작(선·사각형·타원)",
78
+ "entry_points": ("hwpx.document:HwpxDocument",),
79
+ },
80
+ {
81
+ "area": "shape-escape-hatch",
82
+ "matrix_row": "저수준 도형·컨트롤 탈출구",
83
+ "entry_points": ("hwpx.document:HwpxDocument",),
84
+ },
85
+ {
86
+ "area": "curve-objects",
87
+ "matrix_row": "arc·polygon·curve·connectLine",
88
+ "entry_points": (),
89
+ },
90
+ {
91
+ "area": "picture",
92
+ "matrix_row": "그림 삽입/치환",
93
+ "entry_points": ("hwpx.document:HwpxDocument",),
94
+ },
95
+ {
96
+ "area": "chart",
97
+ "matrix_row": "차트",
98
+ "entry_points": ("hwpx.document:HwpxDocument",),
99
+ },
100
+ {
101
+ "area": "equation",
102
+ "matrix_row": "수식",
103
+ "entry_points": ("hwpx.equation.authoring:latex_to_eqedit",),
104
+ },
105
+ {
106
+ "area": "redline",
107
+ "matrix_row": "변경추적(redline)",
108
+ "entry_points": ("hwpx.tools.redline:verify_redline",),
109
+ },
110
+ {
111
+ "area": "memo",
112
+ "matrix_row": "메모(코멘트)",
113
+ "entry_points": ("hwpx.document:HwpxDocument",),
114
+ },
115
+ {
116
+ "area": "footnote-endnote",
117
+ "matrix_row": "각주/미주",
118
+ "entry_points": ("hwpx.document:HwpxDocument",),
119
+ },
120
+ {
121
+ "area": "toc-crossref",
122
+ "matrix_row": "네이티브 목차(TOC)/상호참조",
123
+ "entry_points": ("hwpx.tools.toc_author:add_native_toc",),
124
+ },
125
+ {
126
+ "area": "encrypted-hwpx",
127
+ "matrix_row": "암호화 HWPX",
128
+ "entry_points": (),
129
+ },
130
+ {
131
+ "area": "hwp5-binary",
132
+ "matrix_row": "HWP 5.x 바이너리",
133
+ "entry_points": (),
134
+ },
135
+ {
136
+ "area": "form-field-create",
137
+ "matrix_row": "누름틀(form field) 생성",
138
+ "entry_points": ("hwpx.document:HwpxDocument",),
139
+ },
140
+ )
141
+
142
+ #: 패키지에 동봉되는 계약 문서 이름 → 파일. MCP resources 표면의 원천.
143
+ _CONTRACT_DOCS: dict[str, str] = {
144
+ "support-matrix": "support-matrix.md",
145
+ "recipes-traversal": "recipes-traversal.md",
146
+ "mutation-semantics": "mutation-semantics.md",
147
+ "known-traps": "known-traps.md",
148
+ }
149
+
150
+ _CONTRACT_DOCS_DIR = Path(__file__).resolve().parent / "data" / "contract_docs"
151
+
152
+
153
+ def _extra_installed(extra: str) -> bool:
154
+ """extra 설치 여부 — 함수 스코프 **정적 import** try/except 프로브.
155
+
156
+ core의 importlib 표면은 boundary ratchet상 ``from importlib import
157
+ resources`` 한 형태뿐이라 동적 프로브(find_spec/import_module)를 쓰지
158
+ 않는다. 닫힌 extras 집합이므로 하우스 패턴(mail_merge의 지연 import)과
159
+ 같은 정적 guarded import면 충분하다.
160
+ """
161
+
162
+ if extra == "xlsx":
163
+ try:
164
+ import openpyxl # noqa: F401
165
+ except Exception:
166
+ return False
167
+ return True
168
+ if extra == "preview":
169
+ from .equation.mathml import latex2mathml_available
170
+
171
+ return latex2mathml_available()
172
+ # visual: 5.0부터 의도적 빈 extra(설치 호환 유지) — 공허하게 참.
173
+ return True
174
+
175
+
176
+ def _package_version() -> str:
177
+ from . import __version__
178
+
179
+ return str(__version__)
180
+
181
+
182
+ # 진입점 실해석(import_module)은 런타임에 없다 — core의 동적 import 능력은
183
+ # __init__의 핀된 lazy 로더뿐(boundary ratchet). 레지스트리 진입점이 전부
184
+ # 해석되는지는 tests/test_capabilities_surface.py의 드리프트 가드가 실측한다.
185
+
186
+
187
+ def describe_capabilities() -> dict[str, Any]:
188
+ """설치된 core의 기계가독 자기서술 — ``hwpx.capabilities/v1``.
189
+
190
+ 전부 실측이다: 버전은 배포 메타데이터, extras는 import 프로브, 표면 목록은
191
+ 라이브 ``__all__``, op 어휘는 실행기 디스패치와 같은 표. 렌더 오라클은
192
+ core에 없다는 사실 자체를 보고한다(측정 없는 가용성 주장 금지).
193
+ """
194
+
195
+ from . import __all__ as stable_names
196
+ from .experimental import __all__ as experimental_names
197
+ from .plan import PLAN_OPS
198
+
199
+ return {
200
+ "schemaVersion": CAPABILITIES_SCHEMA,
201
+ "package": {"name": _DIST_NAME, "version": _package_version()},
202
+ "python": platform.python_version(),
203
+ "extras": {name: _extra_installed(name) for name in _EXTRA_MODULES},
204
+ "renderOracle": {
205
+ "bundled": "none",
206
+ "note": (
207
+ "core는 렌더 백엔드를 동봉하지 않습니다(RenderBackend 주입 seam). "
208
+ "실한컴 오라클 탐지·실행과 그 가용성 보고는 python-hwpx-automation"
209
+ "이 소유합니다."
210
+ ),
211
+ },
212
+ "editPlanOps": sorted(PLAN_OPS),
213
+ "schemas": sorted(_SCHEMA_BUILDERS),
214
+ "surfaces": {
215
+ "stable": sorted(stable_names),
216
+ "experimental": sorted(experimental_names),
217
+ },
218
+ "features": [
219
+ {
220
+ "area": row["area"],
221
+ "matrixRow": row["matrix_row"],
222
+ "entryPoints": list(row["entry_points"]),
223
+ }
224
+ for row in _CAPABILITY_AREAS
225
+ ],
226
+ "contractDocuments": sorted(_CONTRACT_DOCS),
227
+ }
228
+
229
+
230
+ def contract_document(name: str) -> str:
231
+ """패키지에 동봉된 계약 문서(markdown 원문)를 돌려준다.
232
+
233
+ 이름 어휘는 ``describe_capabilities()["contractDocuments"]``와 같다. 미지 이름은
234
+ typed 거부(fail-closed).
235
+ """
236
+
237
+ filename = _CONTRACT_DOCS.get(name)
238
+ if filename is None:
239
+ raise HwpxError(
240
+ f"미지 계약 문서 '{name}'입니다.",
241
+ code="unknown-contract-document",
242
+ suggestion=f"가능한 이름: {', '.join(sorted(_CONTRACT_DOCS))}",
243
+ )
244
+ path = _CONTRACT_DOCS_DIR / filename
245
+ try:
246
+ return path.read_text(encoding="utf-8")
247
+ except OSError as exc:
248
+ raise HwpxError(
249
+ f"계약 문서가 패키지에 없습니다: {name}",
250
+ code="contract-document-missing",
251
+ context={"path": str(path)},
252
+ suggestion=(
253
+ "개발 체크아웃이라면 scripts/sync_contract_docs.py를 실행해 "
254
+ "docs/ 원본을 data/contract_docs/로 동기화하세요."
255
+ ),
256
+ ) from exc
257
+
258
+
259
+ def _mutation_report_json_schema() -> dict[str, Any]:
260
+ """``hwpx.mutation-report/v1``의 JSON Schema.
261
+
262
+ 형태의 진실 원천은 :mod:`hwpx.mutation_report`의 ``to_dict()``들이다 —
263
+ 테스트가 실제 리포트 사영을 이 스키마로 검증해 두 표현의 드리프트를 막는다.
264
+ 최상위 필수 키 9개는 automation의 동결 계약(FROZEN_MUTATION_REPORT_KEYS)과
265
+ 같다.
266
+ """
267
+
268
+ verification_value = {"enum": ["passed", "failed", "not_performed"]}
269
+ counts = {
270
+ "type": "object",
271
+ "required": ["verified", "changed"],
272
+ "properties": {
273
+ "verified": {"type": "integer", "minimum": 0},
274
+ "changed": {"type": "integer", "minimum": 0},
275
+ },
276
+ }
277
+ return {
278
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
279
+ "$id": "https://airmang.github.io/python-hwpx/schemas/mutation-report-v1.json",
280
+ "title": "hwpx.mutation-report/v1",
281
+ "type": "object",
282
+ "required": [
283
+ "schemaVersion",
284
+ "ok",
285
+ "path",
286
+ "requestedMode",
287
+ "actualMode",
288
+ "fallbackUsed",
289
+ "changedParts",
290
+ "preservation",
291
+ "verification",
292
+ ],
293
+ "properties": {
294
+ "schemaVersion": {"const": "hwpx.mutation-report/v1"},
295
+ "ok": {"type": "boolean"},
296
+ "path": {"type": ["string", "null"]},
297
+ "requestedMode": {"enum": ["patch", "rebuild", "auto"]},
298
+ "actualMode": {"enum": ["patch", "rebuild"]},
299
+ "fallbackUsed": {"type": "boolean"},
300
+ "changedParts": {
301
+ "type": "array",
302
+ "items": {
303
+ "type": "object",
304
+ "required": ["path", "reason", "ranges"],
305
+ "properties": {
306
+ "path": {"type": "string"},
307
+ "reason": {"enum": ["dirty-part", "unexpected"]},
308
+ "ranges": {
309
+ "type": ["array", "null"],
310
+ "items": {
311
+ "type": "object",
312
+ "required": ["start", "end", "coordinateSpace"],
313
+ "properties": {
314
+ "start": {"type": "integer", "minimum": 0},
315
+ "end": {"type": "integer", "minimum": 0},
316
+ "coordinateSpace": {
317
+ "const": "uncompressed-part-bytes"
318
+ },
319
+ },
320
+ },
321
+ },
322
+ },
323
+ },
324
+ },
325
+ "preservation": {
326
+ "type": "object",
327
+ "required": [
328
+ "untouchedPartPayloads",
329
+ "untouchedLocalZipRecords",
330
+ "wholePackageIdentical",
331
+ ],
332
+ "properties": {
333
+ "untouchedPartPayloads": counts,
334
+ "untouchedLocalZipRecords": counts,
335
+ "wholePackageIdentical": {"type": "boolean"},
336
+ },
337
+ },
338
+ "verification": {
339
+ "type": "object",
340
+ "required": ["package", "openSafety", "reopen", "visual"],
341
+ "properties": {
342
+ "package": verification_value,
343
+ "openSafety": verification_value,
344
+ "reopen": verification_value,
345
+ "visual": verification_value,
346
+ },
347
+ },
348
+ },
349
+ }
350
+
351
+
352
+ def _capabilities_json_schema() -> dict[str, Any]:
353
+ """``hwpx.capabilities/v1``의 JSON Schema."""
354
+
355
+ return {
356
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
357
+ "$id": "https://airmang.github.io/python-hwpx/schemas/capabilities-v1.json",
358
+ "title": CAPABILITIES_SCHEMA,
359
+ "type": "object",
360
+ "required": [
361
+ "schemaVersion",
362
+ "package",
363
+ "python",
364
+ "extras",
365
+ "renderOracle",
366
+ "editPlanOps",
367
+ "schemas",
368
+ "surfaces",
369
+ "features",
370
+ "contractDocuments",
371
+ ],
372
+ "properties": {
373
+ "schemaVersion": {"const": CAPABILITIES_SCHEMA},
374
+ "package": {
375
+ "type": "object",
376
+ "required": ["name", "version"],
377
+ "properties": {
378
+ "name": {"const": _DIST_NAME},
379
+ "version": {"type": "string"},
380
+ },
381
+ },
382
+ "python": {"type": "string"},
383
+ "extras": {
384
+ "type": "object",
385
+ "additionalProperties": {"type": "boolean"},
386
+ },
387
+ "renderOracle": {
388
+ "type": "object",
389
+ "required": ["bundled", "note"],
390
+ "properties": {
391
+ "bundled": {"const": "none"},
392
+ "note": {"type": "string"},
393
+ },
394
+ },
395
+ "editPlanOps": {"type": "array", "items": {"type": "string"}},
396
+ "schemas": {"type": "array", "items": {"type": "string"}},
397
+ "surfaces": {
398
+ "type": "object",
399
+ "required": ["stable", "experimental"],
400
+ "properties": {
401
+ "stable": {"type": "array", "items": {"type": "string"}},
402
+ "experimental": {"type": "array", "items": {"type": "string"}},
403
+ },
404
+ },
405
+ "features": {
406
+ "type": "array",
407
+ "items": {
408
+ "type": "object",
409
+ "required": ["area", "matrixRow", "entryPoints"],
410
+ "properties": {
411
+ "area": {"type": "string"},
412
+ "matrixRow": {"type": "string"},
413
+ "entryPoints": {
414
+ "type": "array",
415
+ "items": {"type": "string"},
416
+ },
417
+ },
418
+ },
419
+ },
420
+ "contractDocuments": {"type": "array", "items": {"type": "string"}},
421
+ },
422
+ }
423
+
424
+
425
+ def _edit_plan_schema() -> dict[str, Any]:
426
+ from .plan import edit_plan_json_schema
427
+
428
+ return edit_plan_json_schema()
429
+
430
+
431
+ def _plan_report_schema() -> dict[str, Any]:
432
+ from .plan import plan_report_json_schema
433
+
434
+ return plan_report_json_schema()
435
+
436
+
437
+ _SCHEMA_BUILDERS: dict[str, Any] = {
438
+ "hwpx.edit-plan/v1": _edit_plan_schema,
439
+ "hwpx.plan-report/v1": _plan_report_schema,
440
+ "hwpx.mutation-report/v1": _mutation_report_json_schema,
441
+ "hwpx.capabilities/v1": _capabilities_json_schema,
442
+ }
443
+
444
+
445
+ def contract_json_schema(name: str) -> dict[str, Any]:
446
+ """계약 스키마를 이름으로 돌려준다(라이브 빌드 — 파일 박제 없음).
447
+
448
+ 이름 어휘는 ``describe_capabilities()["schemas"]``와 같다. 미지 이름은 typed 거부.
449
+ """
450
+
451
+ builder = _SCHEMA_BUILDERS.get(name)
452
+ if builder is None:
453
+ raise HwpxError(
454
+ f"미지 계약 스키마 '{name}'입니다.",
455
+ code="unknown-contract-schema",
456
+ suggestion=f"가능한 이름: {', '.join(sorted(_SCHEMA_BUILDERS))}",
457
+ )
458
+ return builder()
459
+
460
+
461
+ __all__ = [
462
+ "CAPABILITIES_SCHEMA",
463
+ "describe_capabilities",
464
+ "contract_document",
465
+ "contract_json_schema",
466
+ ]