python-hwpx 2.18.0__tar.gz → 2.19.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 (265) hide show
  1. {python_hwpx-2.18.0/src/python_hwpx.egg-info → python_hwpx-2.19.0}/PKG-INFO +1 -1
  2. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/pyproject.toml +1 -1
  3. python_hwpx-2.19.0/src/hwpx/tools/read_fidelity.py +281 -0
  4. python_hwpx-2.19.0/src/hwpx/visual/_hancom_open_rate.ps1 +374 -0
  5. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/oracle.py +185 -0
  6. {python_hwpx-2.18.0 → python_hwpx-2.19.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  7. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/python_hwpx.egg-info/SOURCES.txt +5 -0
  8. python_hwpx-2.19.0/tests/test_corpus_read_fidelity.py +39 -0
  9. python_hwpx-2.19.0/tests/test_openrate.py +560 -0
  10. python_hwpx-2.19.0/tests/test_read_fidelity.py +122 -0
  11. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/LICENSE +0 -0
  12. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/NOTICE +0 -0
  13. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/README.md +0 -0
  14. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/setup.cfg +0 -0
  15. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/__init__.py +0 -0
  16. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/authoring.py +0 -0
  17. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/builder/__init__.py +0 -0
  18. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/builder/core.py +0 -0
  19. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/builder/report.py +0 -0
  20. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/__init__.py +0 -0
  21. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/badges.py +0 -0
  22. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  23. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  24. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  25. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  26. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/corpus.py +0 -0
  27. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/report.py +0 -0
  28. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  29. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/conformance/runner.py +0 -0
  30. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  31. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/__init__.py +0 -0
  32. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/_support.py +0 -0
  33. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/composer.py +0 -0
  34. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/harvest.py +0 -0
  35. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/plan.py +0 -0
  36. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profile.py +0 -0
  37. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  38. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  39. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  40. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  41. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  42. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  43. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
  44. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
  45. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
  46. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
  47. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  48. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  49. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  50. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  51. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  52. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  53. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  54. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  55. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  56. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  57. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  58. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  59. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  60. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/design/validator.py +0 -0
  61. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/document.py +0 -0
  62. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/exam/__init__.py +0 -0
  63. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/exam/compose.py +0 -0
  64. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/exam/ir.py +0 -0
  65. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/exam/measure.py +0 -0
  66. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/exam/parser.py +0 -0
  67. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/exam/profile.py +0 -0
  68. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fill.py +0 -0
  69. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/__init__.py +0 -0
  70. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/apply.py +0 -0
  71. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/engine.py +0 -0
  72. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/measure.py +0 -0
  73. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/policy.py +0 -0
  74. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/report.py +0 -0
  75. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/seal.py +0 -0
  76. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/form_fit/wordbox.py +0 -0
  77. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/layout/__init__.py +0 -0
  78. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/layout/lint.py +0 -0
  79. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/layout/report.py +0 -0
  80. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/opc/package.py +0 -0
  81. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/opc/relationships.py +0 -0
  82. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/opc/security.py +0 -0
  83. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/opc/xml_utils.py +0 -0
  84. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/__init__.py +0 -0
  85. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/_document_impl.py +0 -0
  86. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/body.py +0 -0
  87. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  88. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/common.py +0 -0
  89. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/document.py +0 -0
  90. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/header.py +0 -0
  91. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/header_part.py +0 -0
  92. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/memo.py +0 -0
  93. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/namespaces.py +0 -0
  94. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/numbering.py +0 -0
  95. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/objects.py +0 -0
  96. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/paragraph.py +0 -0
  97. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/parser.py +0 -0
  98. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/run.py +0 -0
  99. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/schema.py +0 -0
  100. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/section.py +0 -0
  101. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/simple_parts.py +0 -0
  102. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/table.py +0 -0
  103. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/oxml/utils.py +0 -0
  104. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/package.py +0 -0
  105. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/patch.py +0 -0
  106. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/presets/__init__.py +0 -0
  107. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/presets/proposal.py +0 -0
  108. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/py.typed +0 -0
  109. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/quality/__init__.py +0 -0
  110. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/quality/ledger.py +0 -0
  111. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/quality/policy.py +0 -0
  112. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/quality/report.py +0 -0
  113. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/quality/save_pipeline.py +0 -0
  114. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/template_formfit.py +0 -0
  115. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/templates.py +0 -0
  116. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/__init__.py +0 -0
  117. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  118. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  119. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/advanced_generators.py +0 -0
  120. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/archive_cli.py +0 -0
  121. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/doc_diff.py +0 -0
  122. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/exporter.py +0 -0
  123. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  124. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  125. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  126. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  127. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  128. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  129. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/generic_inventory.py +0 -0
  130. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/id_integrity.py +0 -0
  131. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/idempotence.py +0 -0
  132. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/ir_equality.py +0 -0
  133. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/layout_preview.py +0 -0
  134. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/mail_merge.py +0 -0
  135. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/markdown_export.py +0 -0
  136. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/object_finder.py +0 -0
  137. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/official_lint.py +0 -0
  138. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/package_reconcile.py +0 -0
  139. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/package_validator.py +0 -0
  140. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/page_guard.py +0 -0
  141. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/pii.py +0 -0
  142. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/recover.py +0 -0
  143. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/redline.py +0 -0
  144. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/repair.py +0 -0
  145. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/report_parser.py +0 -0
  146. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/report_utils.py +0 -0
  147. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  148. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/style_profile.py +0 -0
  149. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/table_cleanup.py +0 -0
  150. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/table_compute.py +0 -0
  151. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/table_navigation.py +0 -0
  152. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/template_analyzer.py +0 -0
  153. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  154. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/text_extractor.py +0 -0
  155. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/tools/validator.py +0 -0
  156. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/__init__.py +0 -0
  157. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  158. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  159. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/detectors.py +0 -0
  160. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/diff.py +0 -0
  161. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/masks.py +0 -0
  162. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/hwpx/visual/report.py +0 -0
  163. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  164. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  165. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  166. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  167. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_advanced_generators.py +0 -0
  168. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_authoring_profile_routing.py +0 -0
  169. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
  170. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_authoring_render_check.py +0 -0
  171. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_builder_core.py +0 -0
  172. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_builder_plan_v2.py +0 -0
  173. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_builder_vertical_slice.py +0 -0
  174. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_comment_node_robustness.py +0 -0
  175. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_conformance.py +0 -0
  176. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_coverage_promotion.py +0 -0
  177. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_coverage_targets.py +0 -0
  178. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_design_builder.py +0 -0
  179. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_deviations_registry.py +0 -0
  180. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_doc_diff.py +0 -0
  181. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_document_context_manager.py +0 -0
  182. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_document_formatting.py +0 -0
  183. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_document_plan.py +0 -0
  184. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_document_plan_computed_fields.py +0 -0
  185. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_document_save_api.py +0 -0
  186. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_exam_compose.py +0 -0
  187. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_exam_compose_oracle.py +0 -0
  188. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_exam_fixtures.py +0 -0
  189. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_exam_ir.py +0 -0
  190. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_exam_measure.py +0 -0
  191. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_exam_parser.py +0 -0
  192. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_exam_profile.py +0 -0
  193. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_existing_document_format_editing.py +0 -0
  194. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_form_fields.py +0 -0
  195. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_form_fill_split_run.py +0 -0
  196. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_form_fit.py +0 -0
  197. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_form_fit_integration.py +0 -0
  198. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_form_fit_seal.py +0 -0
  199. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_form_fit_seal_placement.py +0 -0
  200. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_form_fit_wordbox.py +0 -0
  201. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_fuzz_loop.py +0 -0
  202. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_fuzz_regressions.py +0 -0
  203. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_gap_closure_tools.py +0 -0
  204. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_government_report_preset.py +0 -0
  205. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_government_table_profile.py +0 -0
  206. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_hp_tab_support.py +0 -0
  207. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  208. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_id_generator_range.py +0 -0
  209. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_id_integrity.py +0 -0
  210. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_image_object_workflow.py +0 -0
  211. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_inline_models.py +0 -0
  212. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  213. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_integration_roundtrip.py +0 -0
  214. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_kordoc_absorption.py +0 -0
  215. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_layout_lint.py +0 -0
  216. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_layout_preview.py +0 -0
  217. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_mail_merge_fit.py +0 -0
  218. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_mail_merge_table_compute.py +0 -0
  219. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_mail_merge_xlsx.py +0 -0
  220. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_markdown_export.py +0 -0
  221. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_memo_and_style_editing.py +0 -0
  222. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_namespace_handling.py +0 -0
  223. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_new_features.py +0 -0
  224. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_official_document_style.py +0 -0
  225. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_official_lint_gongmun_gate.py +0 -0
  226. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_official_lint_tableaware.py +0 -0
  227. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_opc_package.py +0 -0
  228. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_open_safety_corpus.py +0 -0
  229. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_oxml_parsing.py +0 -0
  230. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_packaging_license_metadata.py +0 -0
  231. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_packaging_py_typed.py +0 -0
  232. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_paragraph_keep_together.py +0 -0
  233. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_paragraph_section_management.py +0 -0
  234. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_pii.py +0 -0
  235. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_pii_structural.py +0 -0
  236. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_pii_wiring.py +0 -0
  237. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_proposal_preset.py +0 -0
  238. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_question_split_detector.py +0 -0
  239. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_recover_broken_zip.py +0 -0
  240. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_redline_authoring.py +0 -0
  241. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_redline_verify.py +0 -0
  242. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_repair_repack.py +0 -0
  243. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_report_parser.py +0 -0
  244. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_report_utils.py +0 -0
  245. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_repr_snapshots.py +0 -0
  246. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_rhwp_t1_gates.py +0 -0
  247. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_rhwp_t2_verification.py +0 -0
  248. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_roundtrip_fidelity.py +0 -0
  249. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_save_pipeline.py +0 -0
  250. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  251. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_section_headers.py +0 -0
  252. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_set_paragraph_format_keep.py +0 -0
  253. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_skeleton_template_ids.py +0 -0
  254. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_split_merged_cell.py +0 -0
  255. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_style_profile.py +0 -0
  256. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_table_cleanup.py +0 -0
  257. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_table_navigation.py +0 -0
  258. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_tables_default_border.py +0 -0
  259. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_template_analyzer_enrichment.py +0 -0
  260. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_template_formfit.py +0 -0
  261. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_text_extractor_annotations.py +0 -0
  262. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_validation_severity.py +0 -0
  263. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_validator_comment_nodes.py +0 -0
  264. {python_hwpx-2.18.0 → python_hwpx-2.19.0}/tests/test_version_metadata.py +0 -0
  265. {python_hwpx-2.18.0 → python_hwpx-2.19.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.18.0
3
+ Version: 2.19.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 = "2.18.0"
7
+ version = "2.19.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -0,0 +1,281 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Content-level read-fidelity harness for HWPX (M6 / S-060).
3
+
4
+ Where :mod:`hwpx.tools.roundtrip_diff` measures *element-count* preservation,
5
+ this module measures *content* fidelity:
6
+
7
+ * :func:`resolve_run_spans` — the canonical per-run resolved formatting
8
+ (bold / italic / underline / strikeout / color / size / font). This is the
9
+ single source of truth the installed MCP surface is verified against.
10
+ * :func:`collect_notes` — footnote / endnote instances with their body text and
11
+ body run formatting (the reading surfaces drop these today).
12
+ * :func:`roundtrip_fidelity` / :func:`corpus_fidelity` — open->save->reopen
13
+ agreement, the lossless guard.
14
+ * :func:`spans_fidelity` / :func:`notes_fidelity` — general comparators reused
15
+ to score a candidate extraction (e.g. an MCP tool payload) against the
16
+ canonical one.
17
+
18
+ Purely structural — no Hancom oracle required.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any, Sequence
25
+
26
+ from hwpx.document import HwpxDocument
27
+
28
+ _HH = "{http://www.hancom.co.kr/hwpml/2011/head}"
29
+ #: Values that mean "attribute is present but inactive".
30
+ _OFF = {None, "", "NONE"}
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class RunSpan:
35
+ """Resolved inline formatting of a single ``<hp:run>``."""
36
+
37
+ text: str
38
+ bold: bool = False
39
+ italic: bool = False
40
+ underline: str | None = None # underline TYPE (BOTTOM/CENTER/TOP...) or None when off
41
+ strikeout: bool = False
42
+ color: str | None = None
43
+ size_pt: float | None = None
44
+ font: str | None = None
45
+ superscript: bool = False
46
+ subscript: bool = False
47
+
48
+ def to_dict(self) -> dict[str, Any]:
49
+ return {
50
+ "text": self.text,
51
+ "bold": self.bold,
52
+ "italic": self.italic,
53
+ "underline": self.underline,
54
+ "strikeout": self.strikeout,
55
+ "color": self.color,
56
+ "sizePt": self.size_pt,
57
+ "font": self.font,
58
+ "superscript": self.superscript,
59
+ "subscript": self.subscript,
60
+ }
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class NoteSpan:
65
+ """A footnote / endnote instance and its resolved body."""
66
+
67
+ kind: str # "footNote" | "endNote"
68
+ inst_id: str | None
69
+ anchor_para_index: int
70
+ body_text: str
71
+ body_spans: tuple[RunSpan, ...] = field(default_factory=tuple)
72
+
73
+ def to_dict(self) -> dict[str, Any]:
74
+ return {
75
+ "kind": self.kind,
76
+ "instId": self.inst_id,
77
+ "anchorParaIndex": self.anchor_para_index,
78
+ "bodyText": self.body_text,
79
+ "bodySpans": [s.to_dict() for s in self.body_spans],
80
+ }
81
+
82
+
83
+ # ── resolution ───────────────────────────────────────────────────────
84
+ def _fontface_maps(doc: HwpxDocument) -> dict[str, dict[str, str]]:
85
+ """Return ``{lang: {font_id: face_name}}`` from every header's fontfaces."""
86
+ maps: dict[str, dict[str, str]] = {}
87
+ for header in getattr(doc.oxml, "headers", []) or []:
88
+ element = getattr(header, "element", None)
89
+ if element is None:
90
+ continue
91
+ for fontface in element.iter(f"{_HH}fontface"):
92
+ lang = (fontface.get("lang") or "").lower()
93
+ bucket = maps.setdefault(lang, {})
94
+ for font in fontface.findall(f"{_HH}font"):
95
+ fid, face = font.get("id"), font.get("face")
96
+ if fid is not None and face is not None:
97
+ bucket.setdefault(fid, face)
98
+ return maps
99
+
100
+
101
+ def _resolve_font(style: Any, fontfaces: dict[str, dict[str, str]]) -> str | None:
102
+ font_ref = (style.child_attributes.get("fontRef") if style else None) or {}
103
+ fid = font_ref.get("hangul")
104
+ if fid is None:
105
+ fid = next(iter(font_ref.values()), None)
106
+ if fid is None:
107
+ return None
108
+ return fontfaces.get("hangul", {}).get(fid) or None
109
+
110
+
111
+ def _int(value: Any, default: int = 0) -> int:
112
+ try:
113
+ return int(str(value))
114
+ except (TypeError, ValueError):
115
+ return default
116
+
117
+
118
+ def _style_to_span(text: str, style: Any, fontfaces: dict[str, dict[str, str]]) -> RunSpan:
119
+ if style is None:
120
+ return RunSpan(text=text)
121
+ child = style.child_attributes or {}
122
+
123
+ underline_type = (child.get("underline") or {}).get("type")
124
+ if underline_type in _OFF:
125
+ underline_type = None
126
+
127
+ strike_shape = (child.get("strikeout") or {}).get("shape")
128
+ strikeout = strike_shape not in _OFF
129
+
130
+ height = style.attributes.get("height")
131
+ size_pt = round(_int(height) / 100.0, 2) if height and _int(height) > 0 else None
132
+
133
+ offset_h = _int((child.get("offset") or {}).get("hangul"))
134
+
135
+ return RunSpan(
136
+ text=text,
137
+ bold="bold" in child,
138
+ italic="italic" in child,
139
+ underline=underline_type,
140
+ strikeout=strikeout,
141
+ color=style.attributes.get("textColor"),
142
+ size_pt=size_pt,
143
+ font=_resolve_font(style, fontfaces),
144
+ superscript=offset_h > 0,
145
+ subscript=offset_h < 0,
146
+ )
147
+
148
+
149
+ def fontface_maps(doc: HwpxDocument) -> dict[str, dict[str, str]]:
150
+ """Public: ``{lang: {font_id: face_name}}`` for surface reuse (MCP)."""
151
+ return _fontface_maps(doc)
152
+
153
+
154
+ def run_span(text: str, style: Any, fontfaces: dict[str, dict[str, str]] | None = None) -> RunSpan:
155
+ """Public: resolve one run's :class:`RunSpan` (fontfaces from :func:`fontface_maps`)."""
156
+ return _style_to_span(text, style, fontfaces or {})
157
+
158
+
159
+ def resolve_run_spans(doc: HwpxDocument) -> list[RunSpan]:
160
+ """Return the resolved inline formatting for every body run, in order."""
161
+ fontfaces = _fontface_maps(doc)
162
+ spans: list[RunSpan] = []
163
+ for section in doc.sections:
164
+ for paragraph in section.paragraphs:
165
+ for run in paragraph.runs:
166
+ spans.append(_style_to_span(run.text or "", run.style, fontfaces))
167
+ return spans
168
+
169
+
170
+ def collect_notes(doc: HwpxDocument) -> list[NoteSpan]:
171
+ """Return every footnote / endnote with body text and body formatting."""
172
+ fontfaces = _fontface_maps(doc)
173
+ notes: list[NoteSpan] = []
174
+ para_index = 0
175
+ for section in doc.sections:
176
+ for paragraph in section.paragraphs:
177
+ for note in list(paragraph.footnotes) + list(paragraph.endnotes):
178
+ body_spans: tuple[RunSpan, ...] = ()
179
+ try:
180
+ body = note.body_paragraph
181
+ body_spans = tuple(
182
+ _style_to_span(r.text or "", r.style, fontfaces) for r in body.runs
183
+ )
184
+ except Exception: # pragma: no cover - defensive
185
+ body_spans = ()
186
+ notes.append(
187
+ NoteSpan(
188
+ kind=note.kind,
189
+ inst_id=note.inst_id,
190
+ anchor_para_index=para_index,
191
+ body_text=note.text,
192
+ body_spans=body_spans,
193
+ )
194
+ )
195
+ para_index += 1
196
+ return notes
197
+
198
+
199
+ # ── comparators ──────────────────────────────────────────────────────
200
+ def _first_mismatch(ref: Sequence[Any], cand: Sequence[Any]) -> dict[str, Any] | None:
201
+ for i, a in enumerate(ref):
202
+ b = cand[i] if i < len(cand) else None
203
+ if a != b:
204
+ return {"index": i, "ref": a.to_dict() if a is not None else None,
205
+ "cand": b.to_dict() if b is not None else None}
206
+ if len(cand) > len(ref):
207
+ return {"index": len(ref), "ref": None, "cand": cand[len(ref)].to_dict()}
208
+ return None
209
+
210
+
211
+ def spans_fidelity(ref: Sequence[RunSpan], cand: Sequence[RunSpan]) -> dict[str, Any]:
212
+ """Fraction of reference run-spans reproduced identically by ``cand``."""
213
+ denom = max(len(ref), 1)
214
+ same = sum(1 for i, a in enumerate(ref) if i < len(cand) and a == cand[i])
215
+ return {
216
+ "count_ref": len(ref),
217
+ "count_cand": len(cand),
218
+ "same": same,
219
+ "fidelity": same / denom,
220
+ "count_match": len(ref) == len(cand),
221
+ "first_mismatch": _first_mismatch(ref, cand),
222
+ }
223
+
224
+
225
+ def notes_fidelity(ref: Sequence[NoteSpan], cand: Sequence[NoteSpan]) -> dict[str, Any]:
226
+ """Fraction of reference notes reproduced identically by ``cand``."""
227
+ denom = max(len(ref), 1)
228
+ same = sum(1 for i, a in enumerate(ref) if i < len(cand) and a == cand[i])
229
+ return {
230
+ "count_ref": len(ref),
231
+ "count_cand": len(cand),
232
+ "same": same,
233
+ "fidelity": same / denom,
234
+ "count_match": len(ref) == len(cand),
235
+ "first_mismatch": _first_mismatch(ref, cand),
236
+ }
237
+
238
+
239
+ # ── round-trip ───────────────────────────────────────────────────────
240
+ def roundtrip_fidelity(source: str | Path | bytes) -> dict[str, Any]:
241
+ """Open->serialize->reopen and score run-format + note preservation."""
242
+ before = HwpxDocument.open(source)
243
+ runs_before = resolve_run_spans(before)
244
+ notes_before = collect_notes(before)
245
+
246
+ after = HwpxDocument.open(before.to_bytes())
247
+ runs_after = resolve_run_spans(after)
248
+ notes_after = collect_notes(after)
249
+
250
+ return {
251
+ "run_format": spans_fidelity(runs_before, runs_after),
252
+ "notes": notes_fidelity(notes_before, notes_after),
253
+ }
254
+
255
+
256
+ def corpus_fidelity(paths: Sequence[str | Path]) -> dict[str, Any]:
257
+ """Aggregate run-format round-trip fidelity across a corpus."""
258
+ per_file: dict[str, Any] = {}
259
+ total_runs = same_runs = 0
260
+ below: list[dict[str, Any]] = []
261
+ errors: dict[str, str] = {}
262
+ for path in paths:
263
+ key = str(path)
264
+ try:
265
+ report = roundtrip_fidelity(path)["run_format"]
266
+ except Exception as exc: # pragma: no cover - defensive
267
+ errors[key] = repr(exc)[:200]
268
+ continue
269
+ per_file[key] = report
270
+ total_runs += report["count_ref"]
271
+ same_runs += report["same"]
272
+ if report["fidelity"] < 1.0 or not report["count_match"]:
273
+ below.append({"file": key, "fidelity": report["fidelity"], "runs": report["count_ref"]})
274
+ return {
275
+ "files": len(per_file),
276
+ "total_runs": total_runs,
277
+ "run_format_fidelity": (same_runs / total_runs) if total_runs else 1.0,
278
+ "files_below_100pct": below,
279
+ "errors": errors,
280
+ "per_file": per_file,
281
+ }
@@ -0,0 +1,374 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Measure the real-Hancom (한글) OPEN rate of a set of .hwpx files via COM.
4
+
5
+ .DESCRIPTION
6
+ The hardened open-only oracle primitive for the M9 open-rate apparatus
7
+ (specs/007-open-rate). Cloned from scripts/hancom_com_open_verify.ps1 and
8
+ hardened per FR-002:
9
+
10
+ (a) SetMessageBoxMode(0x00020000) so corrupt-file / repair / save modals
11
+ do NOT block a babysat batch.
12
+ (b) Per-file checkpoint: each verdict record is APPENDED to --OutJsonl
13
+ IMMEDIATELY after Open(), so a mid-batch crash loses at most one file.
14
+ (c) RESUME (FR-002, this build): a re-run does NOT truncate the JSONL. On
15
+ startup it reads the existing checkpoint and SKIPS every file with a
16
+ FINAL verdict (a clean load, a clean refusal, or a completed retry). A
17
+ not-yet-retried COM-EXCEPTION error is NOT final and is re-attempted, so a
18
+ run killed mid-retry never freezes a clean file as a permanent failure.
19
+ After a hang/kill the owner re-runs; already-cleared clean popups are not
20
+ shown twice. To force a clean re-measure, delete --OutJsonl first.
21
+ (d) A single retry pass over files that errored on THIS run's pass 1; the
22
+ retried record (retried=$true) is appended and wins the basename join, so
23
+ the aggregator treats a retry-only open as NON-clean (never inflates the
24
+ opens-clean headline).
25
+ (e) Text-scan watchdog: the GetPageText loop (the free "parsed" tier signal)
26
+ is bounded by -OpenTimeoutSec so a pathological page scan cannot stall the
27
+ batch.
28
+
29
+ The script reuses ONE COM session for the common path (Hancom startup
30
+ dominates) but re-creates the session after any COM exception.
31
+
32
+ Open uses the fixed Hancom 2022 (v12) 3-arg signature Open(path,"","")
33
+ (auto-detect format), matching src/hwpx/visual/_render_hwpx.ps1.
34
+
35
+ *** LIMIT — a HUNG Open() ***
36
+ Open() is a synchronous STA COM call; it CANNOT be interrupted in-process
37
+ without killing the Hangul process (which would also kill an unrelated Hangul
38
+ the owner may have open). This script does NOT auto-kill on an Open() hang.
39
+ Mitigation relies on RESUME (c): if an Open() hangs, the babysitter kills the
40
+ run (Ctrl-C / close Hangul), re-runs, and RESUME skips every already-judged
41
+ file. A file that hangs Open() *persistently* is moved aside by the operator
42
+ (it stays unjudged → the aggregator reports it as unverified, coverage-visible).
43
+ An automatic per-file process-kill watchdog is deferred to the TARGET (unattended
44
+ CI) build and must be validated on the box before it is trusted.
45
+
46
+ *** BOX-VERIFICATION REQUIRED (FR-002, opens-clean tier) ***
47
+ SetMessageBoxMode(0x00020000) suppresses the modal, but the SUPPRESSED DEFAULT
48
+ ACTION must NOT be silent auto-repair. If Hancom's default answer to the
49
+ "손상된 파일을 복구하시겠습니까?" dialog is "복구"(repair), a corrupt input would be
50
+ auto-repaired and Open() would return $true — miscounting a broken file as
51
+ opens-clean. This is a Windows/Hancom-build behaviour that CANNOT be checked on
52
+ a Mac. The negative controls (FR-005) are the real check: the must_refuse
53
+ ``synthetic_corrupt_section`` canary is a structurally valid package whose body
54
+ is garbage — if it reports opened=$true, the default action is auto-repair and
55
+ the harness is INVALID (corpus_open_rate.py fails closed). Open the negatives
56
+ FIRST (spike gate) so a leak surfaces before the full sitting.
57
+
58
+ Per-file signal: after a successful Open() the script records ``isModifiedProbe``
59
+ (Hwp.IsModified). This is a BOX-UNVERIFIED auto-repair hint — a clean load
60
+ should read $false. It is recorded for audit only; it is NOT mapped to the
61
+ aggregator's ``repaired`` (non-clean) field until the box run confirms it reads
62
+ $false for known-clean files (measure-first: an unverified property must never
63
+ silently collapse the headline).
64
+
65
+ Windows-only PowerShell (5.1-compatible). Syntactically validated off-box; the
66
+ real run happens on the .161 box.
67
+
68
+ .PARAMETER Path
69
+ One or more .hwpx file paths to open-check.
70
+
71
+ .PARAMETER MaxPages
72
+ Max pages to scan with GetPageText for the free "parsed" (textLength>0) signal.
73
+
74
+ .PARAMETER OutJsonl
75
+ Per-file checkpoint JSONL. One JSON object per line is APPENDED after each
76
+ Open(); a re-run resumes from it (skips already-judged files). Strongly
77
+ recommended for real (babysat) batches.
78
+
79
+ .PARAMETER OutJson
80
+ Optional path to write the final consolidated JSON array (prior + this run).
81
+
82
+ .PARAMETER OpenTimeoutSec
83
+ Bounds the per-file GetPageText scan loop (seconds). Does NOT bound a hung
84
+ Open() (see the LIMIT note above). Default 120.
85
+
86
+ .PARAMETER ProbeRepairMode
87
+ When set (and starting fresh), records the queried SetMessageBoxMode value in a
88
+ leading {"_meta":...} JSONL record for the receipt.
89
+ #>
90
+ param(
91
+ [Parameter(Mandatory = $true)]
92
+ [string[]] $Path,
93
+
94
+ [int] $MaxPages = 20,
95
+
96
+ [string] $OutJsonl = "",
97
+
98
+ [string] $OutJson = "",
99
+
100
+ [int] $OpenTimeoutSec = 120,
101
+
102
+ [switch] $ProbeRepairMode
103
+ )
104
+
105
+ $ErrorActionPreference = "Stop"
106
+ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
107
+
108
+ # 0x00020000 = HWP_MESSAGE_BOX_MODE: auto-answer message boxes (suppress modals).
109
+ $MESSAGE_BOX_MODE = 0x00020000
110
+
111
+ function Resolve-InputPath {
112
+ param([string] $InputPath)
113
+ $resolved = Resolve-Path -LiteralPath $InputPath
114
+ return $resolved.ProviderPath
115
+ }
116
+
117
+ function Copy-ToTrustedTemp {
118
+ param([string] $InputPath)
119
+ $name = [System.IO.Path]::GetFileName($InputPath)
120
+ $dir = Join-Path $env:TEMP ("hwpx-open-rate-" + [System.Guid]::NewGuid().ToString("N"))
121
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
122
+ $target = Join-Path $dir $name
123
+ Copy-Item -LiteralPath $InputPath -Destination $target -Force
124
+ return @{ Directory = $dir; Path = $target }
125
+ }
126
+
127
+ function New-HwpObject {
128
+ $hwp = New-Object -ComObject "HWPFrame.HwpObject"
129
+ try {
130
+ $null = $hwp.RegisterModule("FilePathCheckerModule", "FilePathCheckerModuleExample")
131
+ } catch {
132
+ Write-Warning ("RegisterModule FilePathCheckerModule failed: " + $_.Exception.Message)
133
+ }
134
+ # FR-002a: suppress modal dialogs. The negative controls (FR-005) prove the
135
+ # suppressed default action is NOT silent auto-repair.
136
+ try { $null = $hwp.SetMessageBoxMode($MESSAGE_BOX_MODE) } catch {
137
+ Write-Warning ("SetMessageBoxMode failed: " + $_.Exception.Message)
138
+ }
139
+ return $hwp
140
+ }
141
+
142
+ function Close-HwpObject {
143
+ param([object] $Hwp)
144
+ if ($null -ne $Hwp) {
145
+ try { $Hwp.Quit() | Out-Null } catch {}
146
+ try { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($Hwp) | Out-Null } catch {}
147
+ }
148
+ }
149
+
150
+ function Read-HwpText {
151
+ param(
152
+ [object] $Hwp,
153
+ [int] $PageLimit,
154
+ [int] $TimeoutSec
155
+ )
156
+ # Primary: GetTextFile("TEXT","") — the canonical whole-document text extractor.
157
+ # Box finding 2026-07-01: GetPageText(n) returned 0 chars for ALL real produced
158
+ # docs on the .161 Hancom build (it depends on page-render state), which would
159
+ # collapse the parsed headline to 0%. GetTextFile does not depend on rendering.
160
+ try {
161
+ $whole = $Hwp.GetTextFile("TEXT", "")
162
+ if ($null -ne $whole -and -not [string]::IsNullOrWhiteSpace([string]$whole)) {
163
+ return [string]$whole
164
+ }
165
+ } catch {}
166
+ # Fallback: per-page scan (older builds where GetTextFile is unavailable).
167
+ $parts = New-Object System.Collections.Generic.List[string]
168
+ $sw = [System.Diagnostics.Stopwatch]::StartNew()
169
+ for ($page = 1; $page -le $PageLimit; $page++) {
170
+ # (e) text-scan watchdog: stop scanning if the page loop overruns.
171
+ if ($TimeoutSec -gt 0 -and $sw.Elapsed.TotalSeconds -gt $TimeoutSec) { break }
172
+ try {
173
+ $text = $Hwp.GetPageText($page)
174
+ } catch {
175
+ break
176
+ }
177
+ if ($null -eq $text -or [string]::IsNullOrWhiteSpace([string]$text)) {
178
+ continue
179
+ }
180
+ $parts.Add([string]$text)
181
+ }
182
+ return ($parts -join "`n")
183
+ }
184
+
185
+ # Append one record as a single JSON line to the checkpoint file (FR-002b).
186
+ function Write-Checkpoint {
187
+ param(
188
+ [string] $JsonlPath,
189
+ [object] $Record
190
+ )
191
+ if (-not $JsonlPath) { return }
192
+ $line = ($Record | ConvertTo-Json -Depth 4 -Compress)
193
+ Add-Content -LiteralPath $JsonlPath -Value $line -Encoding UTF8
194
+ }
195
+
196
+ # RESUME (FR-002c): basenames with a FINAL verdict in an existing checkpoint.
197
+ # FINAL = a clean load OR a clean refusal (error is null, opened true or false),
198
+ # OR a record from the single retry pass (retried=true). A COM-EXCEPTION error
199
+ # (error != null) with retried=false is NOT final — it is a to-be-retried state,
200
+ # so a resume RE-ATTEMPTS it. (Without this, a run killed mid-pass-2 would freeze a
201
+ # pass-1 exception as a permanent open_failure and silently DEFLATE the headline —
202
+ # a wrongly-low number is exactly as damaging as a wrongly-high one.) _meta probe
203
+ # lines have no sourcePath and are skipped.
204
+ function Get-JudgedBasenames {
205
+ param([string] $JsonlPath)
206
+ $set = @{}
207
+ if (-not $JsonlPath -or -not (Test-Path -LiteralPath $JsonlPath)) { return $set }
208
+ foreach ($line in (Get-Content -LiteralPath $JsonlPath -Encoding UTF8)) {
209
+ $line = $line.Trim()
210
+ if (-not $line) { continue }
211
+ try { $rec = $line | ConvertFrom-Json } catch { continue }
212
+ if ($rec.PSObject.Properties.Name -contains '_meta') { continue }
213
+ if (-not $rec.sourcePath) { continue }
214
+ $hasError = ($null -ne $rec.error) -and ([string]$rec.error -ne "")
215
+ $retried = [bool]$rec.retried
216
+ if ($hasError -and -not $retried) { continue } # unretried exception -> re-attempt on resume
217
+ $set[[System.IO.Path]::GetFileName([string]$rec.sourcePath)] = $true
218
+ }
219
+ return $set
220
+ }
221
+
222
+ # Prior verdicts (for the consolidated OutJson array).
223
+ function Read-ExistingRecords {
224
+ param([string] $JsonlPath)
225
+ $list = New-Object System.Collections.Generic.List[object]
226
+ if (-not $JsonlPath -or -not (Test-Path -LiteralPath $JsonlPath)) { return $list }
227
+ foreach ($line in (Get-Content -LiteralPath $JsonlPath -Encoding UTF8)) {
228
+ $line = $line.Trim()
229
+ if (-not $line) { continue }
230
+ try { $rec = $line | ConvertFrom-Json } catch { continue }
231
+ if ($rec.PSObject.Properties.Name -contains '_meta') { continue }
232
+ $list.Add($rec)
233
+ }
234
+ return $list
235
+ }
236
+
237
+ # Open one file and return the verdict record. A COM exception is the watchdog
238
+ # trip: the caller re-creates the HwpObject when $record.error is non-null.
239
+ function Invoke-OpenCheck {
240
+ param(
241
+ [object] $Hwp,
242
+ [string] $InputPath,
243
+ [int] $PageLimit,
244
+ [int] $TimeoutSec,
245
+ [bool] $Retried
246
+ )
247
+ $trusted = Copy-ToTrustedTemp $InputPath
248
+ $opened = $false
249
+ $text = ""
250
+ $errorMessage = $null
251
+ $isModifiedProbe = $null
252
+ $pageCount = $null
253
+ try {
254
+ # Hancom 2022 (v12): Open(path, format, arg); ("","") = auto-detect.
255
+ # NOTE (box finding 2026-07-01): Open() returns $true even for container-
256
+ # garbage (not-a-zip/empty/truncated), loading a BLANK doc with textLength=0.
257
+ # So 'opened' alone is NOT the headline — the aggregator uses PARSED
258
+ # (opened AND textLength>0). pageCount is a second "content really loaded"
259
+ # probe captured here so the headline bar can be set from real data.
260
+ $opened = [bool]$Hwp.Open($trusted.Path, "", "")
261
+ if ($opened) {
262
+ # BOX-UNVERIFIED auto-repair hint (recorded for audit; not headline).
263
+ try { $isModifiedProbe = [bool]$Hwp.IsModified } catch { $isModifiedProbe = $null }
264
+ try { $pageCount = [int]$Hwp.PageCount } catch { $pageCount = $null }
265
+ $text = Read-HwpText -Hwp $Hwp -PageLimit $PageLimit -TimeoutSec $TimeoutSec
266
+ }
267
+ } catch {
268
+ $errorMessage = $_.Exception.Message
269
+ } finally {
270
+ try { $Hwp.Clear(1) | Out-Null } catch {}
271
+ Remove-Item -LiteralPath $trusted.Directory -Recurse -Force -ErrorAction SilentlyContinue
272
+ }
273
+ return [ordered]@{
274
+ sourcePath = $InputPath
275
+ opened = $opened
276
+ textLength = $text.Length
277
+ pageCount = $pageCount
278
+ textPreview = if ($text.Length -gt 500) { $text.Substring(0, 500) } else { $text }
279
+ error = $errorMessage
280
+ retried = $Retried
281
+ isModifiedProbe = $isModifiedProbe
282
+ }
283
+ }
284
+
285
+ # RESUME: load prior verdicts + the already-judged basename set. Do NOT truncate.
286
+ $judged = Get-JudgedBasenames $OutJsonl
287
+ $records = New-Object System.Collections.Generic.List[object]
288
+ foreach ($r in (Read-ExistingRecords $OutJsonl)) { $records.Add($r) }
289
+ $resuming = ($judged.Count -gt 0)
290
+ if ($resuming) {
291
+ Write-Host ("RESUME: " + $judged.Count + " file(s) already judged in " + $OutJsonl + " — skipping those.")
292
+ }
293
+
294
+ if ($ProbeRepairMode -and -not $resuming) {
295
+ # Record the queried message-box mode for the receipt (fresh run only).
296
+ $probeHwp = $null
297
+ $modeValue = $null
298
+ $modeError = $null
299
+ try {
300
+ $probeHwp = New-Object -ComObject "HWPFrame.HwpObject"
301
+ try { $modeValue = $probeHwp.SetMessageBoxMode($MESSAGE_BOX_MODE) } catch { $modeError = $_.Exception.Message }
302
+ } catch {
303
+ $modeError = $_.Exception.Message
304
+ } finally {
305
+ Close-HwpObject $probeHwp
306
+ }
307
+ Write-Checkpoint -JsonlPath $OutJsonl -Record ([ordered]@{
308
+ _meta = "repair-mode-probe"
309
+ requestedMode = $MESSAGE_BOX_MODE
310
+ previousModeReturned = $modeValue
311
+ error = $modeError
312
+ note = "Box must confirm suppressed default action is NOT auto-repair; the must_refuse negative controls (FR-005) are the real check."
313
+ })
314
+ }
315
+
316
+ # Pass 1: open every not-yet-judged file, checkpoint immediately, re-create the
317
+ # session after any COM exception.
318
+ $thisRunErrorPaths = New-Object System.Collections.Generic.List[string]
319
+ $hwp = $null
320
+ try {
321
+ $hwp = New-HwpObject
322
+ foreach ($item in $Path) {
323
+ $inputPath = Resolve-InputPath $item
324
+ $base = [System.IO.Path]::GetFileName($inputPath)
325
+ if ($judged.ContainsKey($base)) {
326
+ Write-Host ("skip (already judged): " + $base)
327
+ continue
328
+ }
329
+ $record = Invoke-OpenCheck -Hwp $hwp -InputPath $inputPath -PageLimit $MaxPages -TimeoutSec $OpenTimeoutSec -Retried $false
330
+ Write-Checkpoint -JsonlPath $OutJsonl -Record $record
331
+ $records.Add($record)
332
+ $judged[$base] = $true
333
+ if ($null -ne $record.error) {
334
+ $thisRunErrorPaths.Add($inputPath)
335
+ # Watchdog: a COM error may have poisoned the session — re-create it.
336
+ Close-HwpObject $hwp
337
+ $hwp = New-HwpObject
338
+ }
339
+ }
340
+ } finally {
341
+ Close-HwpObject $hwp
342
+ $hwp = $null
343
+ }
344
+
345
+ # Pass 2 (FR-002d): single retry over files that errored on THIS run's pass 1. A
346
+ # prior-run *unretried* exception is not treated as final — RESUME re-attempts it
347
+ # in pass 1 above (see Get-JudgedBasenames), so it flows here again if it re-errors.
348
+ # The retried record is appended and wins the aggregator's last-line basename join.
349
+ if ($thisRunErrorPaths.Count -gt 0) {
350
+ $hwp = $null
351
+ try {
352
+ $hwp = New-HwpObject
353
+ foreach ($inputPath in $thisRunErrorPaths) {
354
+ $record = Invoke-OpenCheck -Hwp $hwp -InputPath $inputPath -PageLimit $MaxPages -TimeoutSec $OpenTimeoutSec -Retried $true
355
+ Write-Checkpoint -JsonlPath $OutJsonl -Record $record
356
+ $records.Add($record)
357
+ if ($null -ne $record.error) {
358
+ Close-HwpObject $hwp
359
+ $hwp = New-HwpObject
360
+ }
361
+ }
362
+ } finally {
363
+ Close-HwpObject $hwp
364
+ $hwp = $null
365
+ }
366
+ }
367
+
368
+ # Final consolidated array (prior + this run), mirroring the legacy shape.
369
+ $json = $records | ConvertTo-Json -Depth 4
370
+ if ($OutJson) {
371
+ Set-Content -LiteralPath $OutJson -Value $json -Encoding UTF8
372
+ } else {
373
+ $json
374
+ }