python-hwpx 2.13.0__tar.gz → 2.16.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 (253) hide show
  1. {python_hwpx-2.13.0/src/python_hwpx.egg-info → python_hwpx-2.16.0}/PKG-INFO +3 -1
  2. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/pyproject.toml +5 -1
  3. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/__init__.py +2 -0
  4. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/authoring.py +294 -2
  5. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/builder/__init__.py +8 -1
  6. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/builder/core.py +105 -1
  7. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/builder/report.py +80 -0
  8. python_hwpx-2.16.0/src/hwpx/conformance/roundtrip_batch.py +171 -0
  9. python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/fragments/body.xml +1 -0
  10. python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/fragments/heading.xml +1 -0
  11. python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/fragments/title.xml +1 -0
  12. python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/profile.json +24 -0
  13. python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  14. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/document.py +21 -3
  15. python_hwpx-2.16.0/src/hwpx/exam/__init__.py +22 -0
  16. python_hwpx-2.16.0/src/hwpx/exam/compose.py +237 -0
  17. python_hwpx-2.16.0/src/hwpx/exam/ir.py +41 -0
  18. python_hwpx-2.16.0/src/hwpx/exam/measure.py +147 -0
  19. python_hwpx-2.16.0/src/hwpx/exam/parser.py +145 -0
  20. python_hwpx-2.16.0/src/hwpx/exam/profile.py +116 -0
  21. python_hwpx-2.16.0/src/hwpx/form_fit/seal.py +451 -0
  22. python_hwpx-2.16.0/src/hwpx/form_fit/wordbox.py +1212 -0
  23. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/package.py +12 -5
  24. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/_document_impl.py +60 -6
  25. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/body.py +45 -0
  26. python_hwpx-2.16.0/src/hwpx/oxml/canonical_defaults.py +95 -0
  27. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/header.py +16 -2
  28. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/namespaces.py +16 -3
  29. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/utils.py +10 -2
  30. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/id_integrity.py +4 -1
  31. python_hwpx-2.16.0/src/hwpx/tools/idempotence.py +139 -0
  32. python_hwpx-2.16.0/src/hwpx/tools/ir_equality.py +137 -0
  33. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/mail_merge.py +197 -4
  34. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/official_lint.py +111 -6
  35. python_hwpx-2.16.0/src/hwpx/tools/package_reconcile.py +72 -0
  36. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/package_validator.py +16 -6
  37. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/validator.py +6 -3
  38. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/oracle.py +72 -0
  39. {python_hwpx-2.13.0 → python_hwpx-2.16.0/src/python_hwpx.egg-info}/PKG-INFO +3 -1
  40. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/SOURCES.txt +42 -0
  41. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/requires.txt +3 -0
  42. python_hwpx-2.16.0/tests/test_authoring_profile_routing.py +148 -0
  43. python_hwpx-2.16.0/tests/test_authoring_profile_routing_oracle.py +84 -0
  44. python_hwpx-2.16.0/tests/test_authoring_render_check.py +56 -0
  45. python_hwpx-2.16.0/tests/test_comment_node_robustness.py +85 -0
  46. python_hwpx-2.16.0/tests/test_exam_compose.py +85 -0
  47. python_hwpx-2.16.0/tests/test_exam_compose_oracle.py +39 -0
  48. python_hwpx-2.16.0/tests/test_exam_fixtures.py +20 -0
  49. python_hwpx-2.16.0/tests/test_exam_ir.py +19 -0
  50. python_hwpx-2.16.0/tests/test_exam_measure.py +47 -0
  51. python_hwpx-2.16.0/tests/test_exam_parser.py +74 -0
  52. python_hwpx-2.16.0/tests/test_exam_profile.py +39 -0
  53. python_hwpx-2.16.0/tests/test_form_fit_seal.py +125 -0
  54. python_hwpx-2.16.0/tests/test_form_fit_seal_placement.py +378 -0
  55. python_hwpx-2.16.0/tests/test_form_fit_wordbox.py +1096 -0
  56. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_image_object_workflow.py +12 -0
  57. python_hwpx-2.16.0/tests/test_mail_merge_fit.py +129 -0
  58. python_hwpx-2.16.0/tests/test_mail_merge_xlsx.py +40 -0
  59. python_hwpx-2.16.0/tests/test_official_lint_gongmun_gate.py +61 -0
  60. python_hwpx-2.16.0/tests/test_official_lint_tableaware.py +21 -0
  61. python_hwpx-2.16.0/tests/test_paragraph_keep_together.py +46 -0
  62. python_hwpx-2.16.0/tests/test_question_split_detector.py +27 -0
  63. python_hwpx-2.16.0/tests/test_rhwp_t1_gates.py +276 -0
  64. python_hwpx-2.16.0/tests/test_rhwp_t2_verification.py +258 -0
  65. python_hwpx-2.16.0/tests/test_set_paragraph_format_keep.py +73 -0
  66. python_hwpx-2.16.0/tests/test_validator_comment_nodes.py +56 -0
  67. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/LICENSE +0 -0
  68. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/NOTICE +0 -0
  69. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/README.md +0 -0
  70. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/setup.cfg +0 -0
  71. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/__init__.py +0 -0
  72. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/badges.py +0 -0
  73. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  74. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  75. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  76. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  77. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus.py +0 -0
  78. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/report.py +0 -0
  79. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/runner.py +0 -0
  80. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  81. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/__init__.py +0 -0
  82. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/_support.py +0 -0
  83. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/composer.py +0 -0
  84. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/harvest.py +0 -0
  85. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/plan.py +0 -0
  86. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profile.py +0 -0
  87. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  88. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  89. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  90. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  91. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  92. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  93. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  94. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  95. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  96. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  97. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  98. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  99. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  100. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  101. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  102. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  103. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  104. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  105. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/validator.py +0 -0
  106. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fill.py +0 -0
  107. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/__init__.py +0 -0
  108. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/apply.py +0 -0
  109. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/engine.py +0 -0
  110. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/measure.py +0 -0
  111. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/policy.py +0 -0
  112. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/report.py +0 -0
  113. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/layout/__init__.py +0 -0
  114. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/layout/lint.py +0 -0
  115. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/layout/report.py +0 -0
  116. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/relationships.py +0 -0
  117. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/security.py +0 -0
  118. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/xml_utils.py +0 -0
  119. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/__init__.py +0 -0
  120. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/common.py +0 -0
  121. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/document.py +0 -0
  122. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/header_part.py +0 -0
  123. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/memo.py +0 -0
  124. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/numbering.py +0 -0
  125. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/objects.py +0 -0
  126. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/paragraph.py +0 -0
  127. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/parser.py +0 -0
  128. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/run.py +0 -0
  129. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/schema.py +0 -0
  130. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/section.py +0 -0
  131. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/simple_parts.py +0 -0
  132. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/table.py +0 -0
  133. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/package.py +0 -0
  134. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/patch.py +0 -0
  135. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/presets/__init__.py +0 -0
  136. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/presets/proposal.py +0 -0
  137. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/py.typed +0 -0
  138. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/__init__.py +0 -0
  139. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/ledger.py +0 -0
  140. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/policy.py +0 -0
  141. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/report.py +0 -0
  142. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/save_pipeline.py +0 -0
  143. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/template_formfit.py +0 -0
  144. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/templates.py +0 -0
  145. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/__init__.py +0 -0
  146. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  147. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  148. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/advanced_generators.py +0 -0
  149. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/archive_cli.py +0 -0
  150. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/doc_diff.py +0 -0
  151. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/exporter.py +0 -0
  152. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  153. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  154. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  155. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  156. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  157. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  158. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/generic_inventory.py +0 -0
  159. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/layout_preview.py +0 -0
  160. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/markdown_export.py +0 -0
  161. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/object_finder.py +0 -0
  162. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/page_guard.py +0 -0
  163. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/recover.py +0 -0
  164. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/repair.py +0 -0
  165. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/report_parser.py +0 -0
  166. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/report_utils.py +0 -0
  167. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  168. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/style_profile.py +0 -0
  169. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_cleanup.py +0 -0
  170. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_compute.py +0 -0
  171. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_navigation.py +0 -0
  172. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/template_analyzer.py +0 -0
  173. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  174. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/text_extractor.py +0 -0
  175. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/__init__.py +0 -0
  176. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  177. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  178. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/detectors.py +0 -0
  179. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/diff.py +0 -0
  180. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/masks.py +0 -0
  181. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/report.py +0 -0
  182. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  183. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  184. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  185. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_advanced_generators.py +0 -0
  186. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_builder_core.py +0 -0
  187. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_builder_plan_v2.py +0 -0
  188. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_builder_vertical_slice.py +0 -0
  189. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_conformance.py +0 -0
  190. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_coverage_promotion.py +0 -0
  191. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_coverage_targets.py +0 -0
  192. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_design_builder.py +0 -0
  193. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_deviations_registry.py +0 -0
  194. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_doc_diff.py +0 -0
  195. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_context_manager.py +0 -0
  196. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_formatting.py +0 -0
  197. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_plan.py +0 -0
  198. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_plan_computed_fields.py +0 -0
  199. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_save_api.py +0 -0
  200. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_existing_document_format_editing.py +0 -0
  201. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fields.py +0 -0
  202. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fill_split_run.py +0 -0
  203. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fit.py +0 -0
  204. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fit_integration.py +0 -0
  205. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_fuzz_loop.py +0 -0
  206. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_fuzz_regressions.py +0 -0
  207. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_gap_closure_tools.py +0 -0
  208. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_government_report_preset.py +0 -0
  209. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_government_table_profile.py +0 -0
  210. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_hp_tab_support.py +0 -0
  211. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  212. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_id_generator_range.py +0 -0
  213. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_id_integrity.py +0 -0
  214. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_inline_models.py +0 -0
  215. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  216. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_integration_roundtrip.py +0 -0
  217. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_kordoc_absorption.py +0 -0
  218. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_layout_lint.py +0 -0
  219. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_layout_preview.py +0 -0
  220. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_mail_merge_table_compute.py +0 -0
  221. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_markdown_export.py +0 -0
  222. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_memo_and_style_editing.py +0 -0
  223. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_namespace_handling.py +0 -0
  224. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_new_features.py +0 -0
  225. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_official_document_style.py +0 -0
  226. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_opc_package.py +0 -0
  227. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_open_safety_corpus.py +0 -0
  228. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_oxml_parsing.py +0 -0
  229. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_packaging_license_metadata.py +0 -0
  230. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_packaging_py_typed.py +0 -0
  231. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_paragraph_section_management.py +0 -0
  232. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_proposal_preset.py +0 -0
  233. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_recover_broken_zip.py +0 -0
  234. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_repair_repack.py +0 -0
  235. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_report_parser.py +0 -0
  236. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_report_utils.py +0 -0
  237. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_repr_snapshots.py +0 -0
  238. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_roundtrip_fidelity.py +0 -0
  239. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_save_pipeline.py +0 -0
  240. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  241. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_section_headers.py +0 -0
  242. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_skeleton_template_ids.py +0 -0
  243. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_split_merged_cell.py +0 -0
  244. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_style_profile.py +0 -0
  245. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_table_cleanup.py +0 -0
  246. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_table_navigation.py +0 -0
  247. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_tables_default_border.py +0 -0
  248. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_template_analyzer_enrichment.py +0 -0
  249. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_template_formfit.py +0 -0
  250. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_text_extractor_annotations.py +0 -0
  251. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_validation_severity.py +0 -0
  252. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_version_metadata.py +0 -0
  253. {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_visual_oracle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 2.13.0
3
+ Version: 2.16.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -25,6 +25,8 @@ Provides-Extra: visual
25
25
  Requires-Dist: pymupdf>=1.24; extra == "visual"
26
26
  Requires-Dist: pillow>=10.0; extra == "visual"
27
27
  Requires-Dist: numpy>=1.26; extra == "visual"
28
+ Provides-Extra: xlsx
29
+ Requires-Dist: openpyxl>=3.1; extra == "xlsx"
28
30
  Provides-Extra: dev
29
31
  Requires-Dist: build>=1.0; extra == "dev"
30
32
  Requires-Dist: twine>=4.0; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "2.13.0"
7
+ version = "2.16.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -34,6 +34,10 @@ visual = [
34
34
  "pillow>=10.0",
35
35
  "numpy>=1.26",
36
36
  ]
37
+ # Excel(.xlsx/.xlsm) 명부 ingestion for mail_merge. Lazy-imported; absent => clear error.
38
+ xlsx = [
39
+ "openpyxl>=3.1",
40
+ ]
37
41
  dev = [
38
42
  "build>=1.0",
39
43
  "twine>=4.0",
@@ -94,6 +94,7 @@ from .authoring import (
94
94
  PlanValidationIssue,
95
95
  PlanValidationReport,
96
96
  create_document_from_plan,
97
+ get_document_plan_schema,
97
98
  inspect_document_authoring_quality,
98
99
  inspect_operating_plan_quality,
99
100
  normalize_document_plan,
@@ -121,6 +122,7 @@ __all__ = [
121
122
  "DEFAULT_NAMESPACES",
122
123
  "DEFAULT_STYLE_PRESET",
123
124
  "DOCUMENT_PLAN_SCHEMA_VERSION",
125
+ "get_document_plan_schema",
124
126
  "DocumentBlock",
125
127
  "DocumentPlan",
126
128
  "DocumentStylePreset",
@@ -265,6 +265,27 @@ def _plan_issue(
265
265
  )
266
266
 
267
267
 
268
+ _PLAN_FAMILY_PREFIX = "hwpx.document_plan.v"
269
+
270
+
271
+ def _is_forward_plan_version(version: str) -> bool:
272
+ """True if *version* is a newer same-family plan schema (forward-compat).
273
+
274
+ e.g. ``hwpx.document_plan.v3`` when the latest known is v2 — validate
275
+ best-effort with a warning rather than hard-rejecting.
276
+ """
277
+ if not version.startswith(_PLAN_FAMILY_PREFIX):
278
+ return False
279
+ suffix = version[len(_PLAN_FAMILY_PREFIX):]
280
+ if not suffix.isdigit():
281
+ return False
282
+ latest_known = max(
283
+ int(DOCUMENT_PLAN_SCHEMA_VERSION.rsplit("v", 1)[-1]),
284
+ int(DOCUMENT_PLAN_V2_SCHEMA_VERSION.rsplit("v", 1)[-1]),
285
+ )
286
+ return int(suffix) > latest_known
287
+
288
+
268
289
  def _plan_validation_report(
269
290
  issues: list[PlanValidationIssue],
270
291
  *,
@@ -449,6 +470,54 @@ def _plan_repair_hints(issues: tuple[PlanValidationIssue, ...]) -> list[dict[str
449
470
  return hints
450
471
 
451
472
 
473
+ DOCUMENT_PLAN_SCHEMA_ID = "https://airmang.github.io/hwpx-plugins/schemas/document_plan.schema.json"
474
+
475
+
476
+ def get_document_plan_schema() -> dict[str, Any]:
477
+ """Return a JSON Schema (draft 2020-12) for the declarative document plan.
478
+
479
+ Built live from the validator's own constants so it never drifts from the
480
+ accepted contract. Usable directly as an LLM Structured-Outputs / external
481
+ JSON-Schema-validation contract: it constrains the envelope (schemaVersion,
482
+ a non-empty ``blocks`` array, each block carrying a known ``type``) while
483
+ leaving block bodies open (``additionalProperties``) for forward-compat.
484
+ """
485
+
486
+ return {
487
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
488
+ "$id": DOCUMENT_PLAN_SCHEMA_ID,
489
+ "title": "HWPX Document Plan",
490
+ "type": "object",
491
+ "required": ["schemaVersion", "blocks"],
492
+ "additionalProperties": True,
493
+ "properties": {
494
+ "schemaVersion": {
495
+ "type": "string",
496
+ "enum": [DOCUMENT_PLAN_SCHEMA_VERSION, DOCUMENT_PLAN_V2_SCHEMA_VERSION],
497
+ "description": "Plan schema version. Newer same-family versions validate best-effort.",
498
+ },
499
+ "title": {"type": "string"},
500
+ "metadata": {"type": "object"},
501
+ "blocks": {
502
+ "type": "array",
503
+ "minItems": 1,
504
+ "items": {
505
+ "type": "object",
506
+ "required": ["type"],
507
+ "additionalProperties": True,
508
+ "properties": {
509
+ "type": {
510
+ "type": "string",
511
+ "enum": sorted(_SUPPORTED_BLOCK_TYPES),
512
+ "description": "Block kind. Body fields depend on the type.",
513
+ }
514
+ },
515
+ },
516
+ },
517
+ },
518
+ }
519
+
520
+
452
521
  def validate_document_plan(plan: Mapping[str, Any]) -> PlanValidationReport:
453
522
  """Return validation errors for a ``hwpx.document_plan.v1`` mapping."""
454
523
 
@@ -472,6 +541,31 @@ def validate_document_plan(plan: Mapping[str, Any]) -> PlanValidationReport:
472
541
 
473
542
  schema_version = str(plan.get("schemaVersion") or "").strip()
474
543
  if schema_version not in {DOCUMENT_PLAN_SCHEMA_VERSION, DOCUMENT_PLAN_V2_SCHEMA_VERSION}:
544
+ if _is_forward_plan_version(schema_version):
545
+ # Forward-compat: a newer same-family version warns and validates as
546
+ # the latest known schema (best-effort) instead of hard-rejecting, so
547
+ # a plan emitted against a newer schema still generates. Unknown newer
548
+ # fields are simply ignored by the v2 validator.
549
+ issues.append(
550
+ _plan_issue(
551
+ "forward_schema_version",
552
+ "schemaVersion",
553
+ (
554
+ f"schemaVersion {schema_version!r} is newer than the latest "
555
+ f"known {DOCUMENT_PLAN_V2_SCHEMA_VERSION!r}; validating as "
556
+ "latest known (best-effort)."
557
+ ),
558
+ severity="warning",
559
+ suggestion="Unknown newer fields are ignored; verify the output.",
560
+ )
561
+ )
562
+ v2_report = _validate_document_plan_v2(
563
+ plan, schema_version=DOCUMENT_PLAN_V2_SCHEMA_VERSION
564
+ )
565
+ return _plan_validation_report(
566
+ [*issues, *v2_report.issues],
567
+ schema_version=schema_version,
568
+ )
475
569
  issues.append(
476
570
  _plan_issue(
477
571
  "invalid_schema_version",
@@ -780,6 +874,138 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
780
874
  return issues
781
875
 
782
876
 
877
+ # --- M3 (S-057) document-type -> design profile routing ------------------------
878
+ # Maps a plan's document_type (Korean label or profile id) to a committed
879
+ # hwpx.design profile. When it resolves, create_document_from_plan composes from
880
+ # the harvested, Hancom-opens-clean profile skeleton instead of the from-scratch
881
+ # builder. Unknown types keep the legacy from-scratch path (regression-safe).
882
+ _DOCTYPE_TO_PROFILE = {
883
+ "공문": "official_notice",
884
+ "공문서": "official_notice",
885
+ "official_notice": "official_notice",
886
+ "보고서": "report",
887
+ "report": "report",
888
+ "government_report": "report",
889
+ "가정통신문": "home_notice",
890
+ "home_notice": "home_notice",
891
+ }
892
+ _DOCTYPE_METADATA_KEYS = (
893
+ "document_type",
894
+ "문서 유형",
895
+ "문서유형",
896
+ "문서 종류",
897
+ "문서종류",
898
+ "documentType",
899
+ )
900
+ # 결문 (closing block) fields in their canonical render order.
901
+ _GYEOLMUN_FIELDS = (
902
+ ("issuer", "발신명의"),
903
+ ("productionNumber", "생산등록번호"),
904
+ ("enforcementDate", "시행일"),
905
+ ("disclosure", "공개구분"),
906
+ )
907
+
908
+
909
+ def _plan_document_type(plan: Any) -> str:
910
+ """Read the plan's document type from metadata (preferred) or top level."""
911
+
912
+ if not isinstance(plan, Mapping):
913
+ return ""
914
+ metadata = plan.get("metadata")
915
+ metadata = metadata if isinstance(metadata, Mapping) else {}
916
+ for key in _DOCTYPE_METADATA_KEYS:
917
+ value = metadata.get(key) or plan.get(key)
918
+ if value:
919
+ return str(value).strip()
920
+ return ""
921
+
922
+
923
+ def _resolve_design_profile(plan: Any) -> str | None:
924
+ """Return a committed design profile id for the plan's document_type, or None."""
925
+
926
+ raw = _plan_document_type(plan)
927
+ if not raw:
928
+ return None
929
+ from hwpx import design as _design
930
+
931
+ profile_id = _DOCTYPE_TO_PROFILE.get(raw)
932
+ if profile_id and profile_id in _design.available_profiles():
933
+ return profile_id
934
+ return None
935
+
936
+
937
+ def _bridge_to_design_plan(plan: Mapping[str, Any], profile_id: str):
938
+ """Lower a document_plan mapping onto a :class:`hwpx.design.plan.DocumentPlan`.
939
+
940
+ Heading level 1 -> ``heading`` role, level >= 2 -> ``subheading``; paragraphs
941
+ and bullet items -> ``body``; tables -> an ``info`` table block. 결문 메타
942
+ fields are appended as trailing ``body`` blocks in canonical order (P0 proved
943
+ these survive a Hancom render).
944
+ """
945
+
946
+ from hwpx.design.plan import Block as _Block, DocumentPlan as _DesignPlan
947
+
948
+ blocks: list = []
949
+ for raw in plan.get("blocks") or []:
950
+ if not isinstance(raw, Mapping):
951
+ continue
952
+ block_type = str(raw.get("type") or "paragraph")
953
+ if block_type == "heading":
954
+ level = int(raw.get("level") or 1)
955
+ role = "heading" if level <= 1 else "subheading"
956
+ blocks.append(_Block(type="paragraph", role=role, text=str(raw.get("text") or "")))
957
+ elif block_type == "paragraph":
958
+ blocks.append(_Block(type="paragraph", role="body", text=str(raw.get("text") or "")))
959
+ elif block_type == "bullets":
960
+ for item in raw.get("items") or []:
961
+ blocks.append(_Block(type="paragraph", role="body", text=str(item)))
962
+ elif block_type == "table":
963
+ raw_cols = list(raw.get("columns") or raw.get("header") or [])
964
+ if raw_cols and isinstance(raw_cols[0], Mapping):
965
+ # document_plan schema: columns=[{key,label}], rows=[{key: value}]
966
+ keys = [str(c.get("key") or c.get("label") or "") for c in raw_cols]
967
+ columns = [str(c.get("label") or c.get("key") or "") for c in raw_cols]
968
+ rows = []
969
+ for row in raw.get("rows") or []:
970
+ if isinstance(row, Mapping):
971
+ rows.append([str(row.get(k, "")) for k in keys])
972
+ elif isinstance(row, (list, tuple)):
973
+ rows.append([str(c) for c in row])
974
+ else:
975
+ columns = [str(c) for c in raw_cols]
976
+ rows = [[str(c) for c in row] for row in (raw.get("rows") or [])]
977
+ blocks.append(_Block(type="table", role="info", columns=columns, rows=rows))
978
+ # page_break / memo: no design role -> skipped
979
+ gyeolmun = plan.get("gyeolmun")
980
+ if isinstance(gyeolmun, Mapping):
981
+ for key, label in _GYEOLMUN_FIELDS:
982
+ value = gyeolmun.get(key)
983
+ if value:
984
+ blocks.append(_Block(type="paragraph", role="body", text=f"{label} {value}"))
985
+ return _DesignPlan(profile=profile_id, title=str(plan.get("title") or ""), blocks=blocks)
986
+
987
+
988
+ def _korean_proofing_status(plan: Any, normalized_plan: "DocumentPlan | None") -> str:
989
+ """Honest 맞춤법/공공언어 status (Constitution V/IX) — never asserts 'passed'.
990
+
991
+ No free offline Korean spell/spacing oracle exists, so the default is
992
+ ``unverified``. If the plan signals an LLM self-proof pass it is labelled
993
+ ``llm_proofed_not_oracle_verified`` — proofed, but NOT oracle-verified.
994
+ """
995
+
996
+ metadata: Mapping[str, Any] = {}
997
+ if isinstance(plan, Mapping) and isinstance(plan.get("metadata"), Mapping):
998
+ metadata = plan["metadata"]
999
+ elif normalized_plan is not None:
1000
+ metadata = normalized_plan.metadata
1001
+ signal = str(
1002
+ metadata.get("korean_proofing") or metadata.get("korean_proofing_status") or ""
1003
+ ).strip().lower()
1004
+ if signal in {"llm", "llm_proofed", "llm-proofed", "llm_proofed_not_oracle_verified"}:
1005
+ return "llm_proofed_not_oracle_verified"
1006
+ return "unverified"
1007
+
1008
+
783
1009
  def create_document_from_plan(
784
1010
  plan: Mapping[str, Any] | DocumentPlan,
785
1011
  *,
@@ -787,6 +1013,19 @@ def create_document_from_plan(
787
1013
  ) -> HwpxDocument:
788
1014
  """Create a formatted HWPX document from a declarative document plan."""
789
1015
 
1016
+ if isinstance(plan, Mapping):
1017
+ profile_id = _resolve_design_profile(plan)
1018
+ if profile_id is not None:
1019
+ from hwpx import design as _design
1020
+
1021
+ design_plan = _bridge_to_design_plan(plan, profile_id)
1022
+ data, result = _design.compose_bytes(design_plan, production=True)
1023
+ if not result.ok:
1024
+ raise ValueError(
1025
+ f"profile compose failed for {profile_id!r}: {result.errors}"
1026
+ )
1027
+ return HwpxDocument.open(data)
1028
+
790
1029
  normalized = normalize_document_plan(plan)
791
1030
  if normalized.builder_document is not None:
792
1031
  return normalized.builder_document.lower()
@@ -858,8 +1097,15 @@ def inspect_document_authoring_quality(
858
1097
  *,
859
1098
  plan: Mapping[str, Any] | DocumentPlan | None = None,
860
1099
  quality_profile: str | Mapping[str, Any] | None = None,
1100
+ verify_render: bool = False,
861
1101
  ) -> dict[str, Any]:
862
- """Return deterministic structural quality evidence for generated HWPX."""
1102
+ """Return deterministic structural quality evidence for generated HWPX.
1103
+
1104
+ When *verify_render* is true AND a Mac Hancom oracle is reachable, the
1105
+ document is rendered and ``render_checked``/``visual_complete`` become real
1106
+ receipts. Otherwise ``render_checked`` is ``False`` and ``visual_complete``
1107
+ is ``"unverified"`` — never a silent true (Constitution V).
1108
+ """
863
1109
 
864
1110
  normalized_plan: DocumentPlan | None = None
865
1111
  plan_validation: dict[str, Any] | None = None
@@ -887,6 +1133,32 @@ def inspect_document_authoring_quality(
887
1133
  package_report = validate_package(path if path is not None else package_payload)
888
1134
  document_report = document.validate()
889
1135
  reopened = _can_reopen(path, package_payload)
1136
+ render_checked = False
1137
+ visual_complete: Any = "unverified"
1138
+ if verify_render:
1139
+ from hwpx.visual import oracle as _oracle
1140
+
1141
+ _mac = _oracle.MacHancomOracle()
1142
+ if _mac.available():
1143
+ import tempfile as _tf
1144
+
1145
+ with _tf.TemporaryDirectory() as _tmp:
1146
+ _hwpx = Path(_tmp) / "render_check.hwpx"
1147
+ _hwpx.write_bytes(package_payload)
1148
+ _pdf = Path(_tmp) / "render_check.pdf"
1149
+ _rendered = _mac.render_pdf(str(_hwpx), str(_pdf))
1150
+ if _rendered and Path(_rendered).exists():
1151
+ try:
1152
+ import fitz as _fitz
1153
+
1154
+ _doc = _fitz.open(_rendered)
1155
+ _has_text = any(pg.get_text().strip() for pg in _doc)
1156
+ _doc.close()
1157
+ render_checked = bool(_has_text)
1158
+ visual_complete = render_checked
1159
+ except Exception:
1160
+ render_checked = False
1161
+ visual_complete = "unverified"
890
1162
  non_empty_texts = [
891
1163
  (paragraph.text or "").strip()
892
1164
  for paragraph in document.paragraphs
@@ -940,11 +1212,29 @@ def inspect_document_authoring_quality(
940
1212
  and not profiles["operating_plan"].get("pass", False)
941
1213
  ):
942
1214
  gaps.append("operating plan quality failed")
1215
+
1216
+ document_type = ""
1217
+ if isinstance(plan, Mapping):
1218
+ document_type = _plan_document_type(plan)
1219
+ elif normalized_plan is not None:
1220
+ document_type = str(normalized_plan.metadata.get("document_type", "") or "")
1221
+ gongmun_structure: dict[str, Any] | None = None
1222
+ if _DOCTYPE_TO_PROFILE.get(document_type.strip()) == "official_notice":
1223
+ from hwpx.tools.official_lint import (
1224
+ inspect_official_document_style as _gongmun_lint,
1225
+ )
1226
+
1227
+ gongmun_structure = _gongmun_lint(document, document_type="공문")
1228
+ if not gongmun_structure.get("structure_pass", True):
1229
+ gaps.append("공문 structure gate failed")
1230
+ korean_proofing_status = _korean_proofing_status(plan, normalized_plan)
943
1231
  return {
944
1232
  "report_version": AUTHORING_REPORT_VERSION,
945
1233
  "schemaVersion": DOCUMENT_PLAN_SCHEMA_VERSION,
946
1234
  "plan_validation": plan_validation,
947
1235
  "pass": not gaps,
1236
+ "korean_proofing_status": korean_proofing_status,
1237
+ "gongmun_structure": gongmun_structure,
948
1238
  "block_counts": _block_counts(normalized_plan),
949
1239
  "document": {
950
1240
  "paragraph_count": len(document.paragraphs),
@@ -952,6 +1242,8 @@ def inspect_document_authoring_quality(
952
1242
  "table_count": table_count,
953
1243
  "page_break_count": page_break_count,
954
1244
  },
1245
+ "render_checked": render_checked,
1246
+ "visual_complete": visual_complete,
955
1247
  "validation": {
956
1248
  "reopened": reopened,
957
1249
  "validate_package": {
@@ -970,7 +1262,7 @@ def inspect_document_authoring_quality(
970
1262
  "style_token_usage": style_usage,
971
1263
  "recovery": recovery,
972
1264
  "profiles": profiles,
973
- "visual_review_required": bool(gates.get("visualReviewRequired", True)),
1265
+ "visual_review_required": bool(gates.get("visualReviewRequired", True)) and not render_checked,
974
1266
  "gaps": gaps,
975
1267
  }
976
1268
  finally:
@@ -20,10 +20,17 @@ from .core import (
20
20
  Table,
21
21
  approval_box,
22
22
  )
23
- from .report import BuilderSaveReport, ReopenReport
23
+ from .report import (
24
+ FIDELITY_CONTRACT,
25
+ BuilderSaveReport,
26
+ BuilderVerifyReport,
27
+ ReopenReport,
28
+ )
24
29
 
25
30
  __all__ = [
31
+ "FIDELITY_CONTRACT",
26
32
  "BuilderSaveReport",
33
+ "BuilderVerifyReport",
27
34
  "Bullet",
28
35
  "Document",
29
36
  "Footer",
@@ -7,11 +7,14 @@ from pathlib import Path
7
7
  from typing import Any, Mapping, Sequence
8
8
 
9
9
  from hwpx.document import HwpxDocument
10
+ from hwpx.tools.id_integrity import check_id_integrity
11
+ from hwpx.tools.idempotence import IdempotenceReport, check_idempotent_pair
12
+ from hwpx.tools.package_reconcile import reconcile_package_with_document
10
13
  from hwpx.tools.package_validator import validate_editor_open_safety
11
14
  from hwpx.tools.package_validator import validate_package
12
15
  from hwpx.tools.validator import validate_document
13
16
 
14
- from .report import BuilderSaveReport, ReopenReport
17
+ from .report import BuilderSaveReport, BuilderVerifyReport, ReopenReport
15
18
 
16
19
 
17
20
  BuilderChild = (
@@ -805,3 +808,104 @@ class Document:
805
808
  visual_complete=visual_complete,
806
809
  )
807
810
  return report
811
+
812
+ def verify(self) -> BuilderVerifyReport:
813
+ """Dry, no-disk pre-write verification of the built document.
814
+
815
+ Lowers the document to bytes in memory and runs the save hard gates
816
+ (package, document, editor-open-safety, reopen) *plus* id-integrity and
817
+ a two-round idempotence check — a strictly stronger gate set than
818
+ :meth:`save_to_path` (whose report leaves id-integrity to the reader and
819
+ does not check idempotence) — without writing any file. Returns a
820
+ compact signal so a caller can branch on ``ok`` and read a
821
+ section/paragraph count before paying to materialize a real save.
822
+
823
+ Serialization itself can fail (e.g. open-safety rejects the output); in
824
+ that case this returns ``ok=False`` with ``serialize_error`` set rather
825
+ than raising, so a caller (fuzz loop, agent) can always branch on the
826
+ result.
827
+
828
+ See :data:`hwpx.builder.report.FIDELITY_CONTRACT` for what a green
829
+ verdict proves vs. does not prove.
830
+ """
831
+
832
+ try:
833
+ lowered = self.lower()
834
+ data = lowered.to_bytes()
835
+ except Exception as exc: # the document cannot even be serialized
836
+ return BuilderVerifyReport(
837
+ ok=False,
838
+ reopen_ok=False,
839
+ package_ok=False,
840
+ document_ok=False,
841
+ editor_open_safety_ok=False,
842
+ id_integrity_ok=False,
843
+ idempotent=False,
844
+ sections_reconciled=False,
845
+ serialize_error=f"{type(exc).__name__}: {exc}",
846
+ )
847
+
848
+ package_report = validate_package(data)
849
+ document_report = validate_document(data)
850
+ editor_open_safety_report = validate_editor_open_safety(data)
851
+
852
+ reopened: HwpxDocument | None = None
853
+ reopen_error: str | None = None
854
+ try:
855
+ reopened = HwpxDocument.open(data)
856
+ except Exception as exc: # surfaced in the report rather than raised
857
+ reopen_error = f"{type(exc).__name__}: {exc}"
858
+
859
+ id_integrity = (
860
+ check_id_integrity(reopened) if reopened is not None else None
861
+ )
862
+
863
+ # Fixed-point check on the EXACT bytes the gates above validated (gen-1)
864
+ # vs. their reopen-and-resave (gen-2), so the idempotence verdict refers
865
+ # to the bytes we would actually write, not a later generation.
866
+ idempotence: IdempotenceReport | None = None
867
+ serialize_error: str | None = None
868
+ try:
869
+ idempotence = check_idempotent_pair(data, HwpxDocument.open(data).to_bytes())
870
+ except Exception as exc:
871
+ serialize_error = f"{type(exc).__name__}: {exc}"
872
+
873
+ # Output-vs-intent: produced section parts must match the source model.
874
+ reconcile = reconcile_package_with_document(data, lowered)
875
+
876
+ package_ok = bool(getattr(package_report, "ok", False))
877
+ document_ok = bool(getattr(document_report, "ok", False))
878
+ editor_open_safety_ok = bool(getattr(editor_open_safety_report, "ok", False))
879
+ id_integrity_ok = bool(getattr(id_integrity, "ok", False))
880
+ idempotent = bool(idempotence is not None and idempotence.ok)
881
+ reopen_ok = reopened is not None
882
+ section_count = len(reopened.sections) if reopened is not None else 0
883
+ paragraph_count = len(reopened.paragraphs) if reopened is not None else 0
884
+
885
+ ok = (
886
+ package_ok
887
+ and document_ok
888
+ and editor_open_safety_ok
889
+ and id_integrity_ok
890
+ and reopen_ok
891
+ and idempotent
892
+ and reconcile.ok
893
+ )
894
+
895
+ return BuilderVerifyReport(
896
+ ok=ok,
897
+ reopen_ok=reopen_ok,
898
+ package_ok=package_ok,
899
+ document_ok=document_ok,
900
+ editor_open_safety_ok=editor_open_safety_ok,
901
+ id_integrity_ok=id_integrity_ok,
902
+ idempotent=idempotent,
903
+ sections_reconciled=reconcile.ok,
904
+ section_count=section_count,
905
+ paragraph_count=paragraph_count,
906
+ byte_length=len(data),
907
+ reopen_error=reopen_error,
908
+ serialize_error=serialize_error,
909
+ idempotence=idempotence,
910
+ reconcile=reconcile,
911
+ )
@@ -7,10 +7,34 @@ from typing import Any
7
7
 
8
8
  from hwpx.quality import VisualCompleteReport
9
9
  from hwpx.tools.id_integrity import IdIntegrityReport, check_id_integrity
10
+ from hwpx.tools.idempotence import IdempotenceReport
11
+ from hwpx.tools.package_reconcile import PackageReconcileReport
10
12
  from hwpx.tools.package_validator import EditorOpenSafetyReport, PackageValidationReport
11
13
  from hwpx.tools.validator import ValidationReport
12
14
 
13
15
 
16
+ # Explicit scope of what the builder's automated gates prove vs. don't, so a
17
+ # green ``hard_gates`` is never mistaken for full Hancom/visual fidelity. The
18
+ # gates answer "will Hancom likely open this", NOT "did every authored element
19
+ # round-trip". Surfaced in every report's ``to_dict()``.
20
+ FIDELITY_CONTRACT: dict[str, list[str]] = {
21
+ "proves": [
22
+ "package opens as a valid HWPX (mimetype/OPC structure, required entries)",
23
+ "no dangling id references or orphan BinData (id_integrity)",
24
+ "no known editor-open breakage patterns (editor_open_safety)",
25
+ "re-saving reproduces identical part contents (idempotent serialization)",
26
+ "the document reopens with our reader (reopen)",
27
+ ],
28
+ "does_not_prove": [
29
+ "visual layout fidelity in Hancom (line/page breaks, overlap) — needs the "
30
+ "visual oracle / ComputerUse",
31
+ "every authored element round-tripped byte-for-byte: merges, shapes, BinData "
32
+ "bytes, and equation script are not value-diffed",
33
+ "macOS Hancom acceptance for untested element combinations",
34
+ ],
35
+ }
36
+
37
+
14
38
  @dataclass(frozen=True)
15
39
  class ReopenReport:
16
40
  """Result of reopening a generated document."""
@@ -56,6 +80,10 @@ class BuilderSaveReport:
56
80
  "path": str(self.path),
57
81
  "metadata": dict(self.metadata or {}),
58
82
  "hard_gates": dict(self.hard_gates),
83
+ "fidelity_contract": {
84
+ "proves": list(FIDELITY_CONTRACT["proves"]),
85
+ "does_not_prove": list(FIDELITY_CONTRACT["does_not_prove"]),
86
+ },
59
87
  "visual_review_required": self.visual_review_required,
60
88
  "feature_flags": dict(self.feature_flags),
61
89
  "visual_complete": (
@@ -113,3 +141,55 @@ class BuilderSaveReport:
113
141
  }
114
142
  ),
115
143
  }
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class BuilderVerifyReport:
148
+ """Compact, no-disk pre-write verification signal from ``Document.verify()``.
149
+
150
+ Lowers the built document to bytes in memory and runs the same hard gates as
151
+ a real save plus a two-round idempotence check — without writing a file — so
152
+ a caller (agent, fuzz loop) can branch on ``ok`` before committing a path.
153
+ See :data:`FIDELITY_CONTRACT` for what these gates prove vs. don't.
154
+ """
155
+
156
+ ok: bool
157
+ reopen_ok: bool
158
+ package_ok: bool
159
+ document_ok: bool
160
+ editor_open_safety_ok: bool
161
+ id_integrity_ok: bool
162
+ idempotent: bool
163
+ sections_reconciled: bool = True
164
+ section_count: int = 0
165
+ paragraph_count: int = 0
166
+ byte_length: int = 0
167
+ reopen_error: str | None = None
168
+ serialize_error: str | None = None
169
+ idempotence: IdempotenceReport | None = None
170
+ reconcile: PackageReconcileReport | None = None
171
+
172
+ def to_dict(self) -> dict[str, Any]:
173
+ return {
174
+ "ok": self.ok,
175
+ "reopen_ok": self.reopen_ok,
176
+ "package_ok": self.package_ok,
177
+ "document_ok": self.document_ok,
178
+ "editor_open_safety_ok": self.editor_open_safety_ok,
179
+ "id_integrity_ok": self.id_integrity_ok,
180
+ "idempotent": self.idempotent,
181
+ "sections_reconciled": self.sections_reconciled,
182
+ "section_count": self.section_count,
183
+ "paragraph_count": self.paragraph_count,
184
+ "byte_length": self.byte_length,
185
+ "reopen_error": self.reopen_error,
186
+ "serialize_error": self.serialize_error,
187
+ "idempotence": (
188
+ None if self.idempotence is None else self.idempotence.to_dict()
189
+ ),
190
+ "reconcile": (None if self.reconcile is None else self.reconcile.to_dict()),
191
+ "fidelity_contract": {
192
+ "proves": list(FIDELITY_CONTRACT["proves"]),
193
+ "does_not_prove": list(FIDELITY_CONTRACT["does_not_prove"]),
194
+ },
195
+ }