python-hwpx-automation 6.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,750 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Authoring automation handlers for the optional MCP adapter."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import tempfile
8
+ from datetime import date
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from hwpx.tools import report_utils as hwpx_report_utils
13
+ from ..office.authoring.report_parser import (
14
+ parse_government_report_text as parse_hwpx_government_report_text,
15
+ )
16
+
17
+ from .. import quality as quality_contract
18
+ from ..core.content import (
19
+ copy_document_file,
20
+ )
21
+ from ..core.document import create_blank
22
+ from ..document_state import document_state_payload
23
+ from ..office.authoring import (
24
+ create_document_from_plan as build_document_from_plan,
25
+ )
26
+ from ..office.authoring import (
27
+ get_document_plan_schema as get_hwpx_document_plan_schema,
28
+ )
29
+ from ..office.authoring import (
30
+ inspect_document_authoring_quality as inspect_authoring_document_quality,
31
+ )
32
+ from ..office.authoring import (
33
+ normalize_document_plan as normalize_hwpx_document_plan,
34
+ )
35
+ from ..office.authoring import (
36
+ validate_document_plan as validate_hwpx_document_plan,
37
+ )
38
+ from ..office.authoring.presets import (
39
+ create_proposal_document as build_proposal_document,
40
+ )
41
+ from ..office.authoring.presets import (
42
+ inspect_proposal_quality as inspect_proposal_document_quality,
43
+ )
44
+ from ..office.authoring.style_profile import (
45
+ describe_template as describe_hwpx_template,
46
+ )
47
+ from ..office.authoring.style_profile import (
48
+ list_templates as list_hwpx_templates,
49
+ )
50
+ from ..office.authoring.style_profile import (
51
+ register_template as register_hwpx_template,
52
+ )
53
+ from ..office.document_ops import (
54
+ build_comparison_table_plan as build_hwpx_comparison_table_plan,
55
+ )
56
+ from ..quality_generation import (
57
+ create_quality_document_fallback,
58
+ )
59
+ from ..storage import (
60
+ build_hwpx_open_safety_report,
61
+ build_hwpx_verification_report,
62
+ )
63
+ from ..utils.helpers import resolve_path
64
+ from ._shared import (
65
+ _diff_sources,
66
+ _inspect_authoring_quality,
67
+ _proposal_quality_fallback,
68
+ _quality_profile_argument,
69
+ _revision_guard,
70
+ )
71
+
72
+ _VERBOSITY_MODES = {"compact", "full"}
73
+
74
+
75
+ def _normalize_verbosity(verbosity: str | None) -> str:
76
+ value = (verbosity or "compact").strip().lower()
77
+ if value not in _VERBOSITY_MODES:
78
+ expected = ", ".join(sorted(_VERBOSITY_MODES))
79
+ raise ValueError(f"verbosity must be one of: {expected}")
80
+ return value
81
+
82
+
83
+ def _compact_open_safety(open_safety: dict[str, Any] | None) -> dict[str, Any] | None:
84
+ if not isinstance(open_safety, dict):
85
+ return None
86
+ compact: dict[str, Any] = {
87
+ "ok": bool(open_safety.get("ok")),
88
+ "summary": open_safety.get("summary"),
89
+ }
90
+ for key in ("validatePackage", "validateDocument", "reopen"):
91
+ value = open_safety.get(key)
92
+ if isinstance(value, dict):
93
+ compact[key] = {"ok": bool(value.get("ok"))}
94
+ return compact
95
+
96
+
97
+ def _verification_is_successful(verification: dict[str, Any] | None) -> bool:
98
+ if not isinstance(verification, dict):
99
+ return False
100
+ open_safety = verification.get("openSafety")
101
+ if isinstance(open_safety, dict) and not bool(open_safety.get("ok")):
102
+ return False
103
+ return bool(verification.get("ok", True))
104
+
105
+
106
+ def _compact_verification_report(verification: dict[str, Any]) -> dict[str, Any]:
107
+ if not _verification_is_successful(verification):
108
+ return verification
109
+ compact: dict[str, Any] = {
110
+ "ok": bool(verification.get("ok", True)),
111
+ "summary": verification.get("summary", "verification passed"),
112
+ }
113
+ for key in ("filePath", "fileSizeBytes", "warnings"):
114
+ if key in verification:
115
+ compact[key] = verification[key]
116
+ open_safety = _compact_open_safety(verification.get("openSafety"))
117
+ if open_safety is not None:
118
+ compact["openSafety"] = open_safety
119
+ return compact
120
+
121
+
122
+ def _apply_write_verbosity(
123
+ payload: dict[str, Any], verbosity: str | None
124
+ ) -> dict[str, Any]:
125
+ if _normalize_verbosity(verbosity) == "full":
126
+ return payload
127
+ compacted = dict(payload)
128
+ for key in ("verification", "verificationReport"):
129
+ verification = compacted.get(key)
130
+ if isinstance(verification, dict):
131
+ compacted[key] = _compact_verification_report(verification)
132
+ open_safety = compacted.get("openSafety")
133
+ if isinstance(open_safety, dict) and bool(open_safety.get("ok")):
134
+ compacted["openSafety"] = _compact_open_safety(open_safety)
135
+ return compacted
136
+
137
+
138
+ def create_document(
139
+ filename: str,
140
+ title: str = None,
141
+ author: str = None,
142
+ expected_revision: str = None,
143
+ ) -> dict:
144
+ """새 HWPX 문서를 생성하고 즉시 저장합니다."""
145
+ del title, author
146
+ path = resolve_path(filename)
147
+ if Path(path).exists():
148
+ guard = _revision_guard(path, expected_revision)
149
+ if guard is not None:
150
+ return guard
151
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
152
+ verification = create_blank(path)
153
+ return {
154
+ "filename": filename,
155
+ "created": True,
156
+ "verification": verification,
157
+ "openSafety": verification["openSafety"],
158
+ **document_state_payload(path),
159
+ }
160
+
161
+
162
+ def _save_generated_document(doc: Any, path: str, *, quality: Any = None) -> dict:
163
+ target = Path(path)
164
+ target.parent.mkdir(parents=True, exist_ok=True)
165
+ # Phase F: generation also funnels through the one SavePipeline gate.
166
+ quality_contract.assert_write_capability()
167
+ fd, tmp_name = tempfile.mkstemp(
168
+ prefix=f".{target.stem}.",
169
+ suffix=target.suffix or ".hwpx",
170
+ dir=str(target.parent),
171
+ )
172
+ tmp_path = Path(tmp_name)
173
+ try:
174
+ os.close(fd)
175
+ report = quality_contract.save_through_pipeline(doc, tmp_path, quality=quality)
176
+ verification = build_hwpx_verification_report(tmp_path)
177
+ if not verification["openSafety"]["ok"]:
178
+ raise RuntimeError(
179
+ "generated HWPX failed open-safety verification: "
180
+ + verification["openSafety"]["summary"]
181
+ )
182
+ os.replace(tmp_path, target)
183
+ verification["filePath"] = str(target)
184
+ verification["visualComplete"] = quality_contract.visual_complete_block(report)
185
+ return verification
186
+ except BaseException:
187
+ tmp_path.unlink(missing_ok=True)
188
+ raise
189
+
190
+
191
+ def _handoff_status(quality: dict) -> str:
192
+ return "ready" if bool(quality.get("pass")) else "needs_revision"
193
+
194
+
195
+ def _next_action(quality: dict) -> str:
196
+ if bool(quality.get("pass")):
197
+ return "structural handoff is ready; complete visual review before final submission"
198
+ return "review quality.gaps and profile repair_hints, then rerun validate/analyze/create"
199
+
200
+
201
+ def get_document_plan_schema() -> dict:
202
+ """document_plan(생성 계획)의 JSON Schema를 반환합니다.
203
+
204
+ Structured Outputs/외부 검증 계약으로 직접 사용하세요 — 자유형 JSON을 만들고
205
+ validate_document_plan에서 거부당하는 대신, 이 스키마로 생성을 제약합니다.
206
+ """
207
+ if get_hwpx_document_plan_schema is None:
208
+ raise RuntimeError(
209
+ "installed python-hwpx does not provide document-plan authoring"
210
+ )
211
+ return get_hwpx_document_plan_schema()
212
+
213
+
214
+ def validate_document_plan(document_plan: dict) -> dict:
215
+ """선언형 hwpx.document_plan.v1 생성 계획을 검증합니다. 파일은 쓰지 않습니다."""
216
+ if validate_hwpx_document_plan is None or normalize_hwpx_document_plan is None:
217
+ raise RuntimeError(
218
+ "installed python-hwpx does not provide document-plan authoring"
219
+ )
220
+ report = validate_hwpx_document_plan(document_plan or {})
221
+ result = report.to_dict()
222
+ if report.ok:
223
+ result["can_create"] = True
224
+ result["normalizedPlan"] = normalize_hwpx_document_plan(
225
+ document_plan or {}
226
+ ).to_dict()
227
+ result["next_tool"] = "create_document_from_plan"
228
+ else:
229
+ result["can_create"] = False
230
+ result["next_tool"] = "validate_document_plan"
231
+ result["next_action"] = (
232
+ "repair document_plan using repairHints, then rerun validate_document_plan"
233
+ )
234
+ return result
235
+
236
+
237
+ def markdown_to_document_plan(
238
+ markdown: str,
239
+ title: str = None,
240
+ metadata: dict = None,
241
+ style_preset: str = "standard_korean_business",
242
+ ) -> dict:
243
+ """Markdown을 검증 가능한 hwpx.document_plan.v1 초안으로 변환합니다. 파일은 쓰지 않습니다."""
244
+ if validate_hwpx_document_plan is None or normalize_hwpx_document_plan is None:
245
+ raise RuntimeError(
246
+ "installed python-hwpx does not provide document-plan authoring"
247
+ )
248
+ from ..markdown_plan import (
249
+ markdown_to_document_plan as _build_markdown_document_plan,
250
+ )
251
+
252
+ converted = _build_markdown_document_plan(
253
+ markdown or "",
254
+ title=title,
255
+ metadata=metadata or {},
256
+ style_preset=style_preset,
257
+ )
258
+ plan = converted.plan
259
+ report = validate_hwpx_document_plan(plan)
260
+ validation = report.to_dict()
261
+ result: dict[str, Any] = {
262
+ "ok": report.ok,
263
+ "can_create": report.ok,
264
+ "document_plan": plan,
265
+ "validation": validation,
266
+ "warnings": list(converted.warnings),
267
+ "next_tool": "create_document_from_plan"
268
+ if report.ok
269
+ else "markdown_to_document_plan",
270
+ }
271
+ if report.ok:
272
+ result["normalizedPlan"] = normalize_hwpx_document_plan(plan).to_dict()
273
+ else:
274
+ result["next_action"] = (
275
+ "repair Markdown or document_plan using validation.repairHints, then rerun markdown_to_document_plan"
276
+ )
277
+ return result
278
+
279
+
280
+ def analyze_document_plan(
281
+ document_plan: dict,
282
+ destination_filename: str = None,
283
+ style_preset: str = "standard_korean_business",
284
+ quality_profile: str = None,
285
+ profile: dict = None,
286
+ ) -> dict:
287
+ """선언형 document_plan을 파일 쓰기 없이 분석하고 품질 미리보기를 반환합니다."""
288
+ if (
289
+ build_document_from_plan is None
290
+ or validate_hwpx_document_plan is None
291
+ or normalize_hwpx_document_plan is None
292
+ ):
293
+ raise RuntimeError(
294
+ "installed python-hwpx does not provide document-plan authoring"
295
+ )
296
+ validation = validate_hwpx_document_plan(document_plan or {})
297
+ validation_payload = validation.to_dict()
298
+ result = {
299
+ **validation_payload,
300
+ "mutated": False,
301
+ "destination": {
302
+ "filename": destination_filename,
303
+ "path": resolve_path(destination_filename)
304
+ if destination_filename
305
+ else None,
306
+ "required_for_create": bool(destination_filename),
307
+ },
308
+ "style_preset": style_preset,
309
+ "quality_profile": _quality_profile_argument(quality_profile, profile),
310
+ }
311
+ if not validation.ok:
312
+ result.update(
313
+ {
314
+ "can_create": False,
315
+ "handoff_status": "needs_revision",
316
+ "next_tool": "validate_document_plan",
317
+ "next_action": (
318
+ "repair document_plan using repairHints, then rerun analyze_document_plan"
319
+ ),
320
+ }
321
+ )
322
+ return result
323
+
324
+ normalized = normalize_hwpx_document_plan(document_plan or {})
325
+ quality: dict | None = None
326
+ doc = build_document_from_plan(document_plan or {}, preset=style_preset)
327
+ try:
328
+ quality = _inspect_authoring_quality(
329
+ doc,
330
+ document_plan=document_plan or {},
331
+ quality_profile=quality_profile,
332
+ profile=profile,
333
+ )
334
+ finally:
335
+ doc.close()
336
+
337
+ result.update(
338
+ {
339
+ "can_create": True,
340
+ "normalizedPlan": normalized.to_dict(),
341
+ "quality_preview": quality,
342
+ "handoff_status": _handoff_status(quality),
343
+ "next_tool": "create_document_from_plan",
344
+ "next_action": _next_action(quality),
345
+ }
346
+ )
347
+ return result
348
+
349
+
350
+ def create_document_from_plan(
351
+ filename: str,
352
+ document_plan: dict,
353
+ style_preset: str = "standard_korean_business",
354
+ quality_profile: str = None,
355
+ profile: dict = None,
356
+ verify_render: bool = False,
357
+ verbosity: str = "compact",
358
+ ) -> dict:
359
+ """선언형 document_plan으로 HWPX를 생성하고 즉시 저장/검증합니다.
360
+
361
+ document_plan.metadata.document_type 이 '공문'/'보고서'/'가정통신문'이면 실제
362
+ 한컴-harvest 프로파일로 생성됩니다. 공문은 결문 메타
363
+ document_plan.gyeolmun = {issuer, productionNumber, enforcementDate,
364
+ disclosure} 를 지원하고, 공문서 작성규정 구조 hard-gate 결과가
365
+ quality.gongmun_structure(structure_pass)로 반환됩니다. 맞춤법은
366
+ quality.korean_proofing_status(정직 'unverified', 거짓 통과 없음)로 보고합니다.
367
+ 출력은 **HWPX 전용**입니다(.odt/.docx/.pdf 등 비-HWPX 미지원; 기안문 ODT는
368
+ 별도 트랙). verify_render=True 이고 Mac 한컴 오라클이 가용하면
369
+ quality.render_checked/visual_complete 가 실제 렌더 영수증이 됩니다.
370
+ """
371
+ return _create_document_from_plan_impl(
372
+ filename,
373
+ document_plan,
374
+ style_preset=style_preset,
375
+ quality_profile=quality_profile,
376
+ profile=profile,
377
+ verify_render=verify_render,
378
+ verbosity=verbosity,
379
+ )
380
+
381
+
382
+ def _create_document_from_plan_impl(
383
+ filename: str,
384
+ document_plan: dict,
385
+ *,
386
+ style_preset: str = "standard_korean_business",
387
+ quality_profile: str | dict | None = None,
388
+ profile: dict | None = None,
389
+ verify_render: bool = False,
390
+ verbosity: str | None = "compact",
391
+ ) -> dict:
392
+ if (
393
+ build_document_from_plan is None
394
+ or inspect_authoring_document_quality is None
395
+ or validate_hwpx_document_plan is None
396
+ ):
397
+ raise RuntimeError(
398
+ "installed python-hwpx does not provide document-plan authoring"
399
+ )
400
+ # FR-011: HWPX-only output. Non-HWPX formats (ODT/기안문, docx, pdf) are a
401
+ # separate track — never silently attempt them.
402
+ suffix = Path(filename).suffix.lower()
403
+ if suffix and suffix != ".hwpx":
404
+ return {
405
+ "filename": filename,
406
+ "created": False,
407
+ "error": f"unsupported output format {suffix!r}; only .hwpx is supported",
408
+ "handoff_status": "unsupported_format",
409
+ "next_action": "use a .hwpx filename (ODT 기안문 등 비-HWPX 포맷은 별도 트랙)",
410
+ }
411
+ validation = validate_hwpx_document_plan(document_plan or {})
412
+ if not validation.ok:
413
+ return {
414
+ "filename": filename,
415
+ "created": False,
416
+ "error": "document plan failed validation",
417
+ "plan_validation": validation.to_dict(),
418
+ "handoff_status": "needs_revision",
419
+ "next_tool": "validate_document_plan",
420
+ "next_action": (
421
+ "repair document_plan using repairHints, then rerun validate_document_plan"
422
+ ),
423
+ }
424
+ path = resolve_path(filename)
425
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
426
+ doc = build_document_from_plan(document_plan or {}, preset=style_preset)
427
+ try:
428
+ verification = _save_generated_document(doc, path)
429
+ finally:
430
+ doc.close()
431
+ report = _inspect_authoring_quality(
432
+ path,
433
+ document_plan=document_plan or {},
434
+ quality_profile=quality_profile,
435
+ profile=profile,
436
+ verify_render=verify_render,
437
+ )
438
+ result = {
439
+ "filename": filename,
440
+ "created": True,
441
+ "style_preset": style_preset,
442
+ "quality_profile": _quality_profile_argument(quality_profile, profile),
443
+ "plan_validation": validation.to_dict(),
444
+ "handoff_status": _handoff_status(report),
445
+ "next_action": _next_action(report),
446
+ "quality": report,
447
+ "verification": verification,
448
+ }
449
+ return _apply_write_verbosity(result, verbosity)
450
+
451
+
452
+ def create_government_report_document(
453
+ filename: str,
454
+ document_plan: dict,
455
+ profile: dict = None,
456
+ verbosity: str = "compact",
457
+ ) -> dict:
458
+ """정부보고서 프리셋으로 document_plan을 생성하고 즉시 저장/검증합니다."""
459
+ return _create_document_from_plan_impl(
460
+ filename,
461
+ document_plan,
462
+ style_preset="government_report",
463
+ quality_profile="government_report",
464
+ profile=profile,
465
+ verbosity=verbosity,
466
+ )
467
+
468
+
469
+ def _report_values(values: list | tuple | dict | None) -> tuple[list, dict]:
470
+ if isinstance(values, dict):
471
+ args = values.get("args")
472
+ if args is None:
473
+ args = values.get("values")
474
+ if args is None:
475
+ args = []
476
+ if not isinstance(args, list | tuple):
477
+ args = [args]
478
+ kwargs = {
479
+ str(key): value
480
+ for key, value in values.items()
481
+ if key not in {"args", "values"}
482
+ }
483
+ return list(args), kwargs
484
+ if isinstance(values, list | tuple):
485
+ args = list(values)
486
+ if args and isinstance(args[-1], dict):
487
+ kwargs = dict(args.pop())
488
+ else:
489
+ kwargs = {}
490
+ return args, kwargs
491
+ if values is None:
492
+ return [], {}
493
+ return [values], {}
494
+
495
+
496
+ def _optional_int(value: Any, default: int) -> int:
497
+ if value is None:
498
+ return default
499
+ return int(value)
500
+
501
+
502
+ def compute_report_value(
503
+ operation: str,
504
+ values: list | dict = None, # type: ignore[assignment] # Frozen ToolSpec default.
505
+ ) -> dict:
506
+ """정부보고서 표/문장에 넣을 계산값을 python-hwpx report_utils로 계산합니다."""
507
+ if hwpx_report_utils is None:
508
+ raise RuntimeError("installed python-hwpx does not provide report_utils")
509
+
510
+ normalized = str(operation or "").strip().lower().replace("-", "_")
511
+ args, kwargs = _report_values(values)
512
+ try:
513
+ if normalized in {"krw_hangul", "hangul_krw"}:
514
+ value = hwpx_report_utils.format_krw_hangul(args[0])
515
+ elif normalized in {"commas", "number_commas"}:
516
+ value = hwpx_report_utils.format_number_commas(args[0])
517
+ elif normalized == "age":
518
+ today_arg = kwargs.get("today")
519
+ today = date.fromisoformat(today_arg) if today_arg else None
520
+ value = hwpx_report_utils.calculate_age(args[0], today=today)
521
+ elif normalized == "delta":
522
+ value = hwpx_report_utils.format_delta(
523
+ args[0],
524
+ negative_prefix=str(kwargs.get("negative_prefix", "△")),
525
+ )
526
+ elif normalized == "delta_percent":
527
+ value = hwpx_report_utils.format_delta_percent(
528
+ args[0],
529
+ args[1],
530
+ digits=_optional_int(kwargs.get("digits"), 1),
531
+ )
532
+ elif normalized in {"ratio", "ratios"}:
533
+ value = hwpx_report_utils.calculate_ratios(
534
+ args[0],
535
+ args[1],
536
+ digits=_optional_int(kwargs.get("digits"), 1),
537
+ )
538
+ elif normalized in {"date", "normalize_date", "korean_date"}:
539
+ value = hwpx_report_utils.normalize_korean_date(args[0])
540
+ else:
541
+ return {
542
+ "operation": operation,
543
+ "value": None,
544
+ "warnings": [f"unsupported report value operation: {operation}"],
545
+ }
546
+ except (IndexError, KeyError, TypeError, ValueError) as exc:
547
+ return {"operation": operation, "value": None, "warnings": [str(exc)]}
548
+
549
+ return {"operation": normalized, "value": value, "warnings": []}
550
+
551
+
552
+ def register_template(
553
+ name: str,
554
+ source_filename: str,
555
+ registry_path: str = None,
556
+ description: str = "",
557
+ tags: list[str] = None,
558
+ ) -> dict:
559
+ """사용자 템플릿을 등록하고 style profile과 placeholder contract를 저장합니다."""
560
+ if register_hwpx_template is None:
561
+ raise RuntimeError(
562
+ "installed python-hwpx does not provide template registry tools"
563
+ )
564
+ return register_hwpx_template(
565
+ name,
566
+ resolve_path(source_filename),
567
+ registry_path=resolve_path(registry_path) if registry_path else None,
568
+ description=description,
569
+ tags=tags or [],
570
+ )
571
+
572
+
573
+ def list_templates(registry_path: str = None) -> dict:
574
+ """등록된 템플릿 목록을 반환합니다."""
575
+ if list_hwpx_templates is None:
576
+ raise RuntimeError(
577
+ "installed python-hwpx does not provide template registry tools"
578
+ )
579
+ return list_hwpx_templates(
580
+ registry_path=resolve_path(registry_path) if registry_path else None
581
+ )
582
+
583
+
584
+ def describe_template(
585
+ name: str,
586
+ registry_path: str = None,
587
+ values: dict = None,
588
+ ) -> dict:
589
+ """등록 템플릿 상세와 placeholder 미충전 리포트를 반환합니다."""
590
+ if describe_hwpx_template is None:
591
+ raise RuntimeError(
592
+ "installed python-hwpx does not provide template registry tools"
593
+ )
594
+ return describe_hwpx_template(
595
+ name,
596
+ registry_path=resolve_path(registry_path) if registry_path else None,
597
+ values=values or {},
598
+ )
599
+
600
+
601
+ def parse_government_report_text(text: str, title: str = "") -> dict:
602
+ """붙여넣은 정부보고서 텍스트를 document_plan으로 파싱하고 검증합니다."""
603
+ if parse_hwpx_government_report_text is None or validate_hwpx_document_plan is None:
604
+ raise RuntimeError(
605
+ "installed python-hwpx does not provide government-report parsing"
606
+ )
607
+
608
+ document_plan = parse_hwpx_government_report_text(text or "", title=title or "")
609
+ validation = validate_hwpx_document_plan(document_plan)
610
+ result = {
611
+ "document_plan": document_plan,
612
+ "plan_validation": validation.to_dict(),
613
+ "can_create": bool(validation.ok),
614
+ }
615
+ if validation.ok:
616
+ result["next_tool"] = "create_government_report_document"
617
+ result["next_action"] = (
618
+ "review document_plan, then call create_government_report_document"
619
+ )
620
+ else:
621
+ result["next_tool"] = "parse_government_report_text"
622
+ result["next_action"] = (
623
+ "repair source text or document_plan using plan_validation"
624
+ )
625
+ return result
626
+
627
+
628
+ def create_comparison_table_document(
629
+ filename: str,
630
+ old_filename: str = None,
631
+ new_filename: str = None,
632
+ old_paragraphs: list[str] = None,
633
+ new_paragraphs: list[str] = None,
634
+ title: str = "신구대조표",
635
+ include_equal: bool = True,
636
+ verbosity: str = "compact",
637
+ ) -> dict:
638
+ """두 문서/문단을 좌우 신구대조표 HWPX로 생성하고 검증합니다."""
639
+ if (
640
+ build_hwpx_comparison_table_plan is None
641
+ or build_document_from_plan is None
642
+ or validate_hwpx_document_plan is None
643
+ ):
644
+ raise RuntimeError(
645
+ "installed python-hwpx does not provide comparison table generation"
646
+ )
647
+ old_source, new_source = _diff_sources(
648
+ old_filename=old_filename,
649
+ new_filename=new_filename,
650
+ old_paragraphs=old_paragraphs,
651
+ new_paragraphs=new_paragraphs,
652
+ )
653
+ document_plan = build_hwpx_comparison_table_plan(
654
+ old_source,
655
+ new_source,
656
+ title=title,
657
+ include_equal=include_equal,
658
+ )
659
+ validation = validate_hwpx_document_plan(document_plan)
660
+ if not validation.ok:
661
+ return {
662
+ "filename": filename,
663
+ "created": False,
664
+ "error": "comparison table plan failed validation",
665
+ "plan_validation": validation.to_dict(),
666
+ }
667
+ path = resolve_path(filename)
668
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
669
+ doc = build_document_from_plan(document_plan, preset="government_report")
670
+ try:
671
+ verification = _save_generated_document(doc, path)
672
+ finally:
673
+ doc.close()
674
+ result = {
675
+ "filename": filename,
676
+ "created": True,
677
+ "document_plan": document_plan,
678
+ "plan_validation": validation.to_dict(),
679
+ "verification": verification,
680
+ "openSafety": verification.get("openSafety"),
681
+ }
682
+ return _apply_write_verbosity(result, verbosity)
683
+
684
+
685
+ def create_proposal_document(
686
+ filename: str,
687
+ proposal_spec: dict,
688
+ style_preset: str = "clean_korean_proposal",
689
+ verbosity: str = "compact",
690
+ ) -> dict:
691
+ """자연어에서 추출한 proposal_spec으로 제안서형 HWPX 문서를 생성합니다."""
692
+ path = resolve_path(filename)
693
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
694
+ doc = (
695
+ build_proposal_document(proposal_spec or {}, preset=style_preset)
696
+ if build_proposal_document is not None
697
+ else create_quality_document_fallback(proposal_spec or {})
698
+ )
699
+ try:
700
+ verification = _save_generated_document(doc, path)
701
+ finally:
702
+ doc.close()
703
+
704
+ report = (
705
+ inspect_proposal_document_quality(path)
706
+ if inspect_proposal_document_quality is not None
707
+ else _proposal_quality_fallback(path)
708
+ )
709
+ result = {
710
+ "filename": filename,
711
+ "created": True,
712
+ "style_preset": style_preset,
713
+ "quality": report,
714
+ "verification": verification,
715
+ }
716
+ return _apply_write_verbosity(result, verbosity)
717
+
718
+
719
+ def copy_document(source_filename: str, destination_filename: str = None) -> dict:
720
+ """HWPX 문서를 새 경로로 복사합니다. 원본은 유지됩니다."""
721
+ source = resolve_path(source_filename)
722
+ destination = destination_filename
723
+ if destination_filename is not None:
724
+ destination = resolve_path(destination_filename)
725
+ dest = copy_document_file(source, destination)
726
+ open_safety = build_hwpx_open_safety_report(Path(dest))
727
+ return {
728
+ "source": source_filename,
729
+ "destination": os.path.basename(dest),
730
+ "openSafety": open_safety,
731
+ }
732
+
733
+
734
+ __all__ = [
735
+ "create_document",
736
+ "create_document_from_plan",
737
+ "copy_document",
738
+ "create_government_report_document",
739
+ "create_proposal_document",
740
+ "create_comparison_table_document",
741
+ "get_document_plan_schema",
742
+ "validate_document_plan",
743
+ "analyze_document_plan",
744
+ "markdown_to_document_plan",
745
+ "parse_government_report_text",
746
+ "compute_report_value",
747
+ "register_template",
748
+ "list_templates",
749
+ "describe_template",
750
+ ]