python-hwpx 2.15.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.15.0/src/python_hwpx.egg-info → python_hwpx-2.16.0}/PKG-INFO +1 -1
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/pyproject.toml +1 -1
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/authoring.py +200 -2
- 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.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/official_lint.py +111 -6
- {python_hwpx-2.15.0 → python_hwpx-2.16.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/SOURCES.txt +10 -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_official_lint_gongmun_gate.py +61 -0
- python_hwpx-2.16.0/tests/test_official_lint_tableaware.py +21 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/LICENSE +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/NOTICE +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/README.md +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/setup.cfg +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/builder/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/builder/core.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/builder/report.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/badges.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/corpus.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/report.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/conformance/runner.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/_support.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/composer.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/harvest.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/plan.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profile.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/profile.json +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/design/validator.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/document.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/exam/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/exam/compose.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/exam/ir.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/exam/measure.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/exam/parser.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/exam/profile.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/seal.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/form_fit/wordbox.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/package.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/template_formfit.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/__init__.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/detectors.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/diff.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/masks.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/oracle.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/hwpx/visual/report.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_builder_core.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_conformance.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_design_builder.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_document_plan.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_exam_compose.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_exam_compose_oracle.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_exam_fixtures.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_exam_ir.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_exam_measure.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_exam_parser.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_exam_profile.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_form_fit_seal.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_form_fit_seal_placement.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_form_fit_wordbox.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_kordoc_absorption.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_mail_merge_fit.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_new_features.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_question_split_detector.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_rhwp_t1_gates.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_rhwp_t2_verification.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_save_pipeline_no_bypass.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_version_metadata.py +0 -0
- {python_hwpx-2.15.0 → python_hwpx-2.16.0}/tests/test_visual_oracle.py +0 -0
|
@@ -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"
|
|
@@ -874,6 +874,138 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
|
|
|
874
874
|
return issues
|
|
875
875
|
|
|
876
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
|
+
|
|
877
1009
|
def create_document_from_plan(
|
|
878
1010
|
plan: Mapping[str, Any] | DocumentPlan,
|
|
879
1011
|
*,
|
|
@@ -881,6 +1013,19 @@ def create_document_from_plan(
|
|
|
881
1013
|
) -> HwpxDocument:
|
|
882
1014
|
"""Create a formatted HWPX document from a declarative document plan."""
|
|
883
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
|
+
|
|
884
1029
|
normalized = normalize_document_plan(plan)
|
|
885
1030
|
if normalized.builder_document is not None:
|
|
886
1031
|
return normalized.builder_document.lower()
|
|
@@ -952,8 +1097,15 @@ def inspect_document_authoring_quality(
|
|
|
952
1097
|
*,
|
|
953
1098
|
plan: Mapping[str, Any] | DocumentPlan | None = None,
|
|
954
1099
|
quality_profile: str | Mapping[str, Any] | None = None,
|
|
1100
|
+
verify_render: bool = False,
|
|
955
1101
|
) -> dict[str, Any]:
|
|
956
|
-
"""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
|
+
"""
|
|
957
1109
|
|
|
958
1110
|
normalized_plan: DocumentPlan | None = None
|
|
959
1111
|
plan_validation: dict[str, Any] | None = None
|
|
@@ -981,6 +1133,32 @@ def inspect_document_authoring_quality(
|
|
|
981
1133
|
package_report = validate_package(path if path is not None else package_payload)
|
|
982
1134
|
document_report = document.validate()
|
|
983
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"
|
|
984
1162
|
non_empty_texts = [
|
|
985
1163
|
(paragraph.text or "").strip()
|
|
986
1164
|
for paragraph in document.paragraphs
|
|
@@ -1034,11 +1212,29 @@ def inspect_document_authoring_quality(
|
|
|
1034
1212
|
and not profiles["operating_plan"].get("pass", False)
|
|
1035
1213
|
):
|
|
1036
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)
|
|
1037
1231
|
return {
|
|
1038
1232
|
"report_version": AUTHORING_REPORT_VERSION,
|
|
1039
1233
|
"schemaVersion": DOCUMENT_PLAN_SCHEMA_VERSION,
|
|
1040
1234
|
"plan_validation": plan_validation,
|
|
1041
1235
|
"pass": not gaps,
|
|
1236
|
+
"korean_proofing_status": korean_proofing_status,
|
|
1237
|
+
"gongmun_structure": gongmun_structure,
|
|
1042
1238
|
"block_counts": _block_counts(normalized_plan),
|
|
1043
1239
|
"document": {
|
|
1044
1240
|
"paragraph_count": len(document.paragraphs),
|
|
@@ -1046,6 +1242,8 @@ def inspect_document_authoring_quality(
|
|
|
1046
1242
|
"table_count": table_count,
|
|
1047
1243
|
"page_break_count": page_break_count,
|
|
1048
1244
|
},
|
|
1245
|
+
"render_checked": render_checked,
|
|
1246
|
+
"visual_complete": visual_complete,
|
|
1049
1247
|
"validation": {
|
|
1050
1248
|
"reopened": reopened,
|
|
1051
1249
|
"validate_package": {
|
|
@@ -1064,7 +1262,7 @@ def inspect_document_authoring_quality(
|
|
|
1064
1262
|
"style_token_usage": style_usage,
|
|
1065
1263
|
"recovery": recovery,
|
|
1066
1264
|
"profiles": profiles,
|
|
1067
|
-
"visual_review_required": bool(gates.get("visualReviewRequired", True)),
|
|
1265
|
+
"visual_review_required": bool(gates.get("visualReviewRequired", True)) and not render_checked,
|
|
1068
1266
|
"gaps": gaps,
|
|
1069
1267
|
}
|
|
1070
1268
|
finally:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="1" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="26"><hp:t>{{body}}</hp:t></hp:run></hp:p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="1" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="25"><hp:t>{{heading}}</hp:t></hp:run></hp:p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="25" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="17"><hp:t>{{title}}</hp:t></hp:run></hp:p>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "hwpx.design.profile.v1",
|
|
3
|
+
"id": "home_notice",
|
|
4
|
+
"template": "template.hwpx",
|
|
5
|
+
"fragments": {
|
|
6
|
+
"title": "fragments/title.xml",
|
|
7
|
+
"heading": "fragments/heading.xml",
|
|
8
|
+
"body": "fragments/body.xml"
|
|
9
|
+
},
|
|
10
|
+
"page": {
|
|
11
|
+
"width": 59528,
|
|
12
|
+
"height": 84186,
|
|
13
|
+
"orientation": "WIDELY",
|
|
14
|
+
"margins": {
|
|
15
|
+
"left": 4251,
|
|
16
|
+
"right": 4251,
|
|
17
|
+
"top": 1417,
|
|
18
|
+
"bottom": 0
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"char_pr_count": 54,
|
|
22
|
+
"style_coverage_threshold": 0.98,
|
|
23
|
+
"source_basename": "ganghwa_records.hwpx"
|
|
24
|
+
}
|
|
Binary file
|
|
@@ -36,28 +36,48 @@ _ATTACHMENT_RE = re.compile(r"^\s*(?:붙임|첨부)\s+(?:\d+\.\s*)?.+\s+\d+\s*
|
|
|
36
36
|
_SPACE_BEFORE_PUNCTUATION_RE = re.compile(r"\s+[:??]")
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def inspect_official_document_style(
|
|
40
|
-
|
|
39
|
+
def inspect_official_document_style(
|
|
40
|
+
source: Any, *, document_type: Any = None
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""Inspect official-document conventions in text, plans, or HWPX files.
|
|
43
|
+
|
|
44
|
+
When *document_type* resolves to a 공문 (official outgoing document) the
|
|
45
|
+
structural spine — 두문(수신)·결문(발신명의·시행·공개구분)·끝. — is enforced at
|
|
46
|
+
ERROR severity (the hard-gate, ``structure_pass``). Without *document_type*
|
|
47
|
+
the behaviour is unchanged (backward compatible).
|
|
48
|
+
"""
|
|
41
49
|
|
|
42
50
|
paragraphs = _paragraphs_from_source(source)
|
|
51
|
+
is_gongmun = _is_gongmun(document_type)
|
|
43
52
|
violations: list[dict[str, Any]] = []
|
|
44
53
|
violations.extend(_inspect_marker_hierarchy(paragraphs))
|
|
45
|
-
|
|
54
|
+
if not is_gongmun:
|
|
55
|
+
# A 시행문 places its 결문(발신명의·시행) AFTER the 끝. marker, so the strict
|
|
56
|
+
# "끝. must be the final paragraph" rule does not apply to 공문; the
|
|
57
|
+
# structure gate enforces 끝. presence instead.
|
|
58
|
+
violations.extend(_inspect_end_marker(paragraphs))
|
|
46
59
|
violations.extend(_inspect_attachment_notation(paragraphs))
|
|
47
60
|
violations.extend(_inspect_dates(paragraphs))
|
|
48
61
|
violations.extend(_inspect_amounts(paragraphs))
|
|
49
62
|
violations.extend(_inspect_spacing(paragraphs))
|
|
63
|
+
if is_gongmun:
|
|
64
|
+
violations.extend(_inspect_gongmun_structure(paragraphs))
|
|
50
65
|
|
|
51
66
|
violation_count = len(violations)
|
|
67
|
+
error_count = sum(1 for v in violations if v.get("severity") == "error")
|
|
52
68
|
ok = violation_count == 0
|
|
69
|
+
rules = list(_RULES_CHECKED) + (list(_GONGMUN_STRUCTURE_RULES) if is_gongmun else [])
|
|
53
70
|
return {
|
|
54
71
|
"report_version": OFFICIAL_DOCUMENT_STYLE_REPORT_VERSION,
|
|
55
72
|
"pass": ok,
|
|
73
|
+
"structure_pass": error_count == 0,
|
|
74
|
+
"document_type": str(document_type) if document_type else None,
|
|
56
75
|
"score": max(0.0, round(1.0 - (violation_count / 10), 2)),
|
|
57
76
|
"summary": {
|
|
58
77
|
"paragraph_count": len(paragraphs),
|
|
59
78
|
"violation_count": violation_count,
|
|
60
|
-
"
|
|
79
|
+
"error_count": error_count,
|
|
80
|
+
"rules_checked": rules,
|
|
61
81
|
},
|
|
62
82
|
"violations": violations,
|
|
63
83
|
"repair_hints": [
|
|
@@ -71,9 +91,94 @@ def inspect_official_document_style(source: Any) -> dict[str, Any]:
|
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
|
|
94
|
+
_GONGMUN_DOCTYPES = {"공문", "공문서", "official_notice", "시행문"}
|
|
95
|
+
_GONGMUN_STRUCTURE_RULES = (
|
|
96
|
+
"missing-susin",
|
|
97
|
+
"missing-balsinmyeongui",
|
|
98
|
+
"missing-sihaeng",
|
|
99
|
+
"missing-disclosure",
|
|
100
|
+
"missing-end-marker",
|
|
101
|
+
)
|
|
102
|
+
_ISSUER_SUFFIX_RE = re.compile(r"(장|관|감)$")
|
|
103
|
+
_DISCLOSURE_RE = re.compile(r"(부분공개|비공개|공개)")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_gongmun(document_type: Any) -> bool:
|
|
107
|
+
return bool(document_type) and str(document_type).strip() in _GONGMUN_DOCTYPES
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _norm_spaces(text: str) -> str:
|
|
111
|
+
return re.sub(r"\s+", "", text)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _inspect_gongmun_structure(paragraphs: Sequence[str]) -> list[dict[str, Any]]:
|
|
115
|
+
"""ERROR-severity 공문 spine checks (the hard-gate), anchored by a real 시행문.
|
|
116
|
+
|
|
117
|
+
Reliably machine-checkable from real 시행문: 수신(두문), 시행/공개구분(결문),
|
|
118
|
+
끝.(본문 종결), and 발신명의 — detected via the literal label OR a 기관장 명의
|
|
119
|
+
line (space-normalised, ending 장/관/감, not the 수신 recipient line).
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
nonempty = [t.strip() for t in paragraphs if t.strip()]
|
|
123
|
+
norm = [_norm_spaces(t) for t in nonempty]
|
|
124
|
+
full_norm = "".join(norm)
|
|
125
|
+
violations: list[dict[str, Any]] = []
|
|
126
|
+
|
|
127
|
+
def err(rule: str, message: str, suggestion: str) -> None:
|
|
128
|
+
violations.append(
|
|
129
|
+
_violation(
|
|
130
|
+
rule=rule,
|
|
131
|
+
paragraph_index=0,
|
|
132
|
+
text="",
|
|
133
|
+
message=message,
|
|
134
|
+
suggestion=suggestion,
|
|
135
|
+
severity="error",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if "수신" not in full_norm:
|
|
140
|
+
err("missing-susin", "공문 두문에 수신(수신자)이 없습니다",
|
|
141
|
+
"두문에 '수신 <수신자>'를 추가하세요.")
|
|
142
|
+
if "시행" not in full_norm:
|
|
143
|
+
err("missing-sihaeng", "공문 결문에 시행 정보가 없습니다",
|
|
144
|
+
"결문에 '시행 <처리과-일련번호> (<시행일자>)'를 추가하세요.")
|
|
145
|
+
if not _DISCLOSURE_RE.search(full_norm):
|
|
146
|
+
err("missing-disclosure", "공문 결문에 공개구분이 없습니다",
|
|
147
|
+
"결문에 공개구분(공개/부분공개/비공개)을 추가하세요.")
|
|
148
|
+
if "끝." not in full_norm:
|
|
149
|
+
err("missing-end-marker", "공문 본문에 끝 표시(끝.)가 없습니다",
|
|
150
|
+
"본문/붙임 마지막에 '끝.'을 두세요.")
|
|
151
|
+
has_label = "발신명의" in full_norm
|
|
152
|
+
has_issuer = any(
|
|
153
|
+
_ISSUER_SUFFIX_RE.search(t) and len(t) >= 3 and "수신" not in t and not t.endswith(")")
|
|
154
|
+
for t in norm
|
|
155
|
+
)
|
|
156
|
+
if not (has_label or has_issuer):
|
|
157
|
+
err("missing-balsinmyeongui", "공문 결문에 발신명의(기관장 명의)가 없습니다",
|
|
158
|
+
"결문에 발신명의(예: ○○교육지원청교육장)를 추가하세요.")
|
|
159
|
+
return violations
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _document_paragraph_texts(paragraphs: Any) -> list[str]:
|
|
163
|
+
"""Flatten paragraph text including nested table-cell text.
|
|
164
|
+
|
|
165
|
+
Real 시행문 carry the 두문(수신·경유) and 결문(발신명의·시행·공개구분) inside
|
|
166
|
+
tables, which top-level ``document.paragraphs`` does not descend into.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
texts: list[str] = []
|
|
170
|
+
for paragraph in paragraphs:
|
|
171
|
+
texts.append(paragraph.text)
|
|
172
|
+
for table in getattr(paragraph, "tables", ()):
|
|
173
|
+
for row in table.rows:
|
|
174
|
+
for cell in row.cells:
|
|
175
|
+
texts.extend(_document_paragraph_texts(cell.paragraphs))
|
|
176
|
+
return texts
|
|
177
|
+
|
|
178
|
+
|
|
74
179
|
def _paragraphs_from_source(source: Any) -> list[str]:
|
|
75
180
|
if isinstance(source, HwpxDocument):
|
|
76
|
-
return
|
|
181
|
+
return _document_paragraph_texts(source.paragraphs)
|
|
77
182
|
if isinstance(source, Path):
|
|
78
183
|
return _paragraphs_from_path(source)
|
|
79
184
|
if isinstance(source, str):
|
|
@@ -96,7 +201,7 @@ def _paragraphs_from_source(source: Any) -> list[str]:
|
|
|
96
201
|
def _paragraphs_from_path(path: Path) -> list[str]:
|
|
97
202
|
document = HwpxDocument.open(path)
|
|
98
203
|
try:
|
|
99
|
-
return
|
|
204
|
+
return _document_paragraph_texts(document.paragraphs)
|
|
100
205
|
finally:
|
|
101
206
|
document.close()
|
|
102
207
|
|
|
@@ -38,6 +38,11 @@ src/hwpx/design/profiles/application_form/fragments/body.xml
|
|
|
38
38
|
src/hwpx/design/profiles/application_form/fragments/heading.xml
|
|
39
39
|
src/hwpx/design/profiles/application_form/fragments/info_table.xml
|
|
40
40
|
src/hwpx/design/profiles/application_form/fragments/title.xml
|
|
41
|
+
src/hwpx/design/profiles/home_notice/profile.json
|
|
42
|
+
src/hwpx/design/profiles/home_notice/template.hwpx
|
|
43
|
+
src/hwpx/design/profiles/home_notice/fragments/body.xml
|
|
44
|
+
src/hwpx/design/profiles/home_notice/fragments/heading.xml
|
|
45
|
+
src/hwpx/design/profiles/home_notice/fragments/title.xml
|
|
41
46
|
src/hwpx/design/profiles/official_notice/profile.json
|
|
42
47
|
src/hwpx/design/profiles/official_notice/template.hwpx
|
|
43
48
|
src/hwpx/design/profiles/official_notice/fragments/body.xml
|
|
@@ -151,6 +156,9 @@ src/python_hwpx.egg-info/entry_points.txt
|
|
|
151
156
|
src/python_hwpx.egg-info/requires.txt
|
|
152
157
|
src/python_hwpx.egg-info/top_level.txt
|
|
153
158
|
tests/test_advanced_generators.py
|
|
159
|
+
tests/test_authoring_profile_routing.py
|
|
160
|
+
tests/test_authoring_profile_routing_oracle.py
|
|
161
|
+
tests/test_authoring_render_check.py
|
|
154
162
|
tests/test_builder_core.py
|
|
155
163
|
tests/test_builder_plan_v2.py
|
|
156
164
|
tests/test_builder_vertical_slice.py
|
|
@@ -205,6 +213,8 @@ tests/test_memo_and_style_editing.py
|
|
|
205
213
|
tests/test_namespace_handling.py
|
|
206
214
|
tests/test_new_features.py
|
|
207
215
|
tests/test_official_document_style.py
|
|
216
|
+
tests/test_official_lint_gongmun_gate.py
|
|
217
|
+
tests/test_official_lint_tableaware.py
|
|
208
218
|
tests/test_opc_package.py
|
|
209
219
|
tests/test_open_safety_corpus.py
|
|
210
220
|
tests/test_oxml_parsing.py
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""M3 P1 — document_type -> design profile routing + 결문 IR bridge."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from hwpx.authoring import (
|
|
6
|
+
_resolve_design_profile,
|
|
7
|
+
_bridge_to_design_plan,
|
|
8
|
+
create_document_from_plan,
|
|
9
|
+
)
|
|
10
|
+
from hwpx.design.plan import DocumentPlan as DesignPlan
|
|
11
|
+
from hwpx.document import HwpxDocument
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _plan(**over):
|
|
15
|
+
base = {
|
|
16
|
+
"schemaVersion": "hwpx.document_plan.v1",
|
|
17
|
+
"title": "교육협력 사업 추진 협조 요청",
|
|
18
|
+
"metadata": {"document_type": "공문"},
|
|
19
|
+
"blocks": [
|
|
20
|
+
{"type": "heading", "level": 1, "text": "1. 관련"},
|
|
21
|
+
{"type": "paragraph", "text": "가. 협조하여 주시기 바랍니다. 끝."},
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
base.update(over)
|
|
25
|
+
return base
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --- Task 1: resolver ---
|
|
29
|
+
def test_resolve_known_korean_types():
|
|
30
|
+
assert _resolve_design_profile(_plan()) == "official_notice"
|
|
31
|
+
assert _resolve_design_profile(_plan(metadata={"document_type": "보고서"})) == "report"
|
|
32
|
+
assert _resolve_design_profile(_plan(metadata={"document_type": "가정통신문"})) == "home_notice"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_resolve_profile_id_direct():
|
|
36
|
+
assert _resolve_design_profile(_plan(metadata={"document_type": "official_notice"})) == "official_notice"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_resolve_unknown_returns_none():
|
|
40
|
+
assert _resolve_design_profile(_plan(metadata={"document_type": "메모"})) is None
|
|
41
|
+
assert _resolve_design_profile(_plan(metadata={})) is None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --- Task 2: bridge ---
|
|
45
|
+
def test_bridge_maps_title_and_roles():
|
|
46
|
+
dp = _bridge_to_design_plan(_plan(), "official_notice")
|
|
47
|
+
assert isinstance(dp, DesignPlan)
|
|
48
|
+
assert dp.profile == "official_notice"
|
|
49
|
+
assert dp.title == "교육협력 사업 추진 협조 요청"
|
|
50
|
+
roles = [(b.type, b.role) for b in dp.blocks]
|
|
51
|
+
assert ("paragraph", "heading") in roles
|
|
52
|
+
assert ("paragraph", "body") in roles
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_bridge_level2_is_subheading():
|
|
56
|
+
dp = _bridge_to_design_plan(_plan(blocks=[{"type": "heading", "level": 2, "text": "x"}]), "report")
|
|
57
|
+
assert ("paragraph", "subheading") in [(b.type, b.role) for b in dp.blocks]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_bridge_converts_mapping_table():
|
|
61
|
+
# document_plan tables are mapping-based: columns=[{key,label}], rows=[{key:value}]
|
|
62
|
+
plan = _plan(blocks=[{
|
|
63
|
+
"type": "table",
|
|
64
|
+
"columns": [{"key": "dept", "label": "부서"}, {"key": "rate", "label": "달성률"}],
|
|
65
|
+
"rows": [{"dept": "기획부", "rate": "100%"}, {"dept": "운영부", "rate": "93%"}],
|
|
66
|
+
}])
|
|
67
|
+
dp = _bridge_to_design_plan(plan, "report")
|
|
68
|
+
table = [b for b in dp.blocks if b.type == "table"][0]
|
|
69
|
+
assert table.columns == ["부서", "달성률"]
|
|
70
|
+
assert table.rows == [["기획부", "100%"], ["운영부", "93%"]]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_bridge_bullets_become_body():
|
|
74
|
+
dp = _bridge_to_design_plan(
|
|
75
|
+
_plan(blocks=[{"type": "bullets", "items": ["가. 첫째", "나. 둘째"]}]), "official_notice"
|
|
76
|
+
)
|
|
77
|
+
bodies = [b.text for b in dp.blocks if b.role == "body"]
|
|
78
|
+
assert "가. 첫째" in bodies and "나. 둘째" in bodies
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --- Task 3: 결문 메타 ---
|
|
82
|
+
def test_bridge_appends_gyeolmun():
|
|
83
|
+
plan = _plan(gyeolmun={
|
|
84
|
+
"issuer": "○○교육지원청교육장",
|
|
85
|
+
"productionNumber": "교육협력과-123",
|
|
86
|
+
"enforcementDate": "2026. 6. 27.",
|
|
87
|
+
"disclosure": "공개",
|
|
88
|
+
})
|
|
89
|
+
dp = _bridge_to_design_plan(plan, "official_notice")
|
|
90
|
+
texts = " ".join(b.text for b in dp.blocks)
|
|
91
|
+
assert "○○교육지원청교육장" in texts
|
|
92
|
+
assert "교육협력과-123" in texts
|
|
93
|
+
assert "2026. 6. 27." in texts
|
|
94
|
+
assert "공개" in texts
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --- Task 4: route (contract preserved) ---
|
|
98
|
+
def test_gongmun_routes_to_profile_and_opens():
|
|
99
|
+
doc = create_document_from_plan(_plan())
|
|
100
|
+
assert isinstance(doc, HwpxDocument)
|
|
101
|
+
text = "\n".join(p.text for p in doc.paragraphs)
|
|
102
|
+
assert "협조" in text
|
|
103
|
+
doc.close()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_unknown_type_uses_from_scratch_path():
|
|
107
|
+
doc = create_document_from_plan(_plan(metadata={"document_type": "메모"}))
|
|
108
|
+
assert isinstance(doc, HwpxDocument)
|
|
109
|
+
doc.close()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --- Task 3: authoring quality surfaces 공문 gate + korean_proofing_status ---
|
|
113
|
+
def _quality_of(plan):
|
|
114
|
+
import tempfile
|
|
115
|
+
from pathlib import Path
|
|
116
|
+
from hwpx.authoring import inspect_document_authoring_quality
|
|
117
|
+
|
|
118
|
+
doc = create_document_from_plan(plan)
|
|
119
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
120
|
+
f = Path(tmp) / "g.hwpx"
|
|
121
|
+
doc.save_to_path(str(f))
|
|
122
|
+
doc.close()
|
|
123
|
+
return inspect_document_authoring_quality(str(f), plan=plan)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_quality_surfaces_gongmun_structure_and_proofing():
|
|
127
|
+
plan = _plan(
|
|
128
|
+
blocks=[{"type": "paragraph", "text": "수신 각급학교장"},
|
|
129
|
+
{"type": "heading", "level": 1, "text": "1. 관련"},
|
|
130
|
+
{"type": "paragraph", "text": "가. 협조하여 주시기 바랍니다. 끝."}],
|
|
131
|
+
gyeolmun={"issuer": "○○교육지원청교육장", "enforcementDate": "2026. 6. 27.", "disclosure": "공개"},
|
|
132
|
+
)
|
|
133
|
+
rep = _quality_of(plan)
|
|
134
|
+
assert rep["korean_proofing_status"] == "unverified"
|
|
135
|
+
assert rep["gongmun_structure"] is not None
|
|
136
|
+
assert rep["gongmun_structure"]["structure_pass"] is True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_quality_proofing_llm_label():
|
|
140
|
+
plan = _plan(
|
|
141
|
+
metadata={"document_type": "공문", "korean_proofing": "llm_proofed"},
|
|
142
|
+
blocks=[{"type": "paragraph", "text": "수신 각급학교장"},
|
|
143
|
+
{"type": "heading", "level": 1, "text": "1. 관련"},
|
|
144
|
+
{"type": "paragraph", "text": "가. 협조. 끝."}],
|
|
145
|
+
gyeolmun={"issuer": "○○교육지원청교육장", "enforcementDate": "2026. 6. 27.", "disclosure": "공개"},
|
|
146
|
+
)
|
|
147
|
+
rep = _quality_of(plan)
|
|
148
|
+
assert rep["korean_proofing_status"] == "llm_proofed_not_oracle_verified"
|