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.
- {python_hwpx-2.13.0/src/python_hwpx.egg-info → python_hwpx-2.16.0}/PKG-INFO +3 -1
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/pyproject.toml +5 -1
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/__init__.py +2 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/authoring.py +294 -2
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/builder/__init__.py +8 -1
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/builder/core.py +105 -1
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/builder/report.py +80 -0
- python_hwpx-2.16.0/src/hwpx/conformance/roundtrip_batch.py +171 -0
- python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/fragments/body.xml +1 -0
- python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/fragments/heading.xml +1 -0
- python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/fragments/title.xml +1 -0
- python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/profile.json +24 -0
- python_hwpx-2.16.0/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/document.py +21 -3
- python_hwpx-2.16.0/src/hwpx/exam/__init__.py +22 -0
- python_hwpx-2.16.0/src/hwpx/exam/compose.py +237 -0
- python_hwpx-2.16.0/src/hwpx/exam/ir.py +41 -0
- python_hwpx-2.16.0/src/hwpx/exam/measure.py +147 -0
- python_hwpx-2.16.0/src/hwpx/exam/parser.py +145 -0
- python_hwpx-2.16.0/src/hwpx/exam/profile.py +116 -0
- python_hwpx-2.16.0/src/hwpx/form_fit/seal.py +451 -0
- python_hwpx-2.16.0/src/hwpx/form_fit/wordbox.py +1212 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/package.py +12 -5
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/_document_impl.py +60 -6
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/body.py +45 -0
- python_hwpx-2.16.0/src/hwpx/oxml/canonical_defaults.py +95 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/header.py +16 -2
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/namespaces.py +16 -3
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/utils.py +10 -2
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/id_integrity.py +4 -1
- python_hwpx-2.16.0/src/hwpx/tools/idempotence.py +139 -0
- python_hwpx-2.16.0/src/hwpx/tools/ir_equality.py +137 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/mail_merge.py +197 -4
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/official_lint.py +111 -6
- python_hwpx-2.16.0/src/hwpx/tools/package_reconcile.py +72 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/package_validator.py +16 -6
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/validator.py +6 -3
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/oracle.py +72 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0/src/python_hwpx.egg-info}/PKG-INFO +3 -1
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/SOURCES.txt +42 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/requires.txt +3 -0
- python_hwpx-2.16.0/tests/test_authoring_profile_routing.py +148 -0
- python_hwpx-2.16.0/tests/test_authoring_profile_routing_oracle.py +84 -0
- python_hwpx-2.16.0/tests/test_authoring_render_check.py +56 -0
- python_hwpx-2.16.0/tests/test_comment_node_robustness.py +85 -0
- python_hwpx-2.16.0/tests/test_exam_compose.py +85 -0
- python_hwpx-2.16.0/tests/test_exam_compose_oracle.py +39 -0
- python_hwpx-2.16.0/tests/test_exam_fixtures.py +20 -0
- python_hwpx-2.16.0/tests/test_exam_ir.py +19 -0
- python_hwpx-2.16.0/tests/test_exam_measure.py +47 -0
- python_hwpx-2.16.0/tests/test_exam_parser.py +74 -0
- python_hwpx-2.16.0/tests/test_exam_profile.py +39 -0
- python_hwpx-2.16.0/tests/test_form_fit_seal.py +125 -0
- python_hwpx-2.16.0/tests/test_form_fit_seal_placement.py +378 -0
- python_hwpx-2.16.0/tests/test_form_fit_wordbox.py +1096 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_image_object_workflow.py +12 -0
- python_hwpx-2.16.0/tests/test_mail_merge_fit.py +129 -0
- python_hwpx-2.16.0/tests/test_mail_merge_xlsx.py +40 -0
- python_hwpx-2.16.0/tests/test_official_lint_gongmun_gate.py +61 -0
- python_hwpx-2.16.0/tests/test_official_lint_tableaware.py +21 -0
- python_hwpx-2.16.0/tests/test_paragraph_keep_together.py +46 -0
- python_hwpx-2.16.0/tests/test_question_split_detector.py +27 -0
- python_hwpx-2.16.0/tests/test_rhwp_t1_gates.py +276 -0
- python_hwpx-2.16.0/tests/test_rhwp_t2_verification.py +258 -0
- python_hwpx-2.16.0/tests/test_set_paragraph_format_keep.py +73 -0
- python_hwpx-2.16.0/tests/test_validator_comment_nodes.py +56 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/LICENSE +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/NOTICE +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/README.md +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/setup.cfg +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/badges.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/report.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/conformance/runner.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/_support.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/composer.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/harvest.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/plan.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profile.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/profile.json +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/design/validator.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/package.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/template_formfit.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/__init__.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/detectors.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/diff.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/masks.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/hwpx/visual/report.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_builder_core.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_conformance.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_design_builder.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_plan.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_kordoc_absorption.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_new_features.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_save_pipeline_no_bypass.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-2.13.0 → python_hwpx-2.16.0}/tests/test_version_metadata.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
+
}
|