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,1439 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """:mod:`python-hwpx` 위에 구축한 고수준 연산 호환 facade."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, cast
10
+
11
+ from .core.plan import PlanManager
12
+ from .core.locator import RegisteredHandle
13
+ from .storage import DocumentStorage, LocalDocumentStorage
14
+ from .workspace import WorkspaceMissingParentGuard, WorkspaceOutputGuard
15
+ from .upstream import HH_NS as HH_NS
16
+ from .upstream import HP_NS as HP_NS
17
+ from .upstream import (
18
+ HwpxDocument,
19
+ HwpxOxmlMemo,
20
+ HwpxOxmlParagraph,
21
+ HwpxOxmlTable,
22
+ )
23
+ from .errors import build_error_payload
24
+ from .ops_services.composition import HwpxOpsServices, build_hwpx_ops_services
25
+ from .ops_services.context import DocumentContext
26
+ from .ops_services.save_policy import (
27
+ SavePolicy,
28
+ _ExactBackupResult,
29
+ _ExactRecoveryPublication,
30
+ _ExactSidecarMutation,
31
+ )
32
+ from .ops_services.tables import DEFAULT_PAGING_PARAGRAPH_LIMIT
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class HwpxOperationError(RuntimeError):
38
+ """문서 단위 작업이 실패했을 때 사용하는 예외."""
39
+
40
+ def __init__(
41
+ self,
42
+ message: str,
43
+ *,
44
+ code: str = "OPERATION_FAILED",
45
+ details: Optional[Dict[str, Any]] = None,
46
+ hint: Optional[str] = None,
47
+ ) -> None:
48
+ super().__init__(message)
49
+ self.code = code
50
+ self.message = message
51
+ self.details = details
52
+ self.hint = hint
53
+
54
+ def to_payload(self) -> Dict[str, Any]:
55
+ return build_error_payload(
56
+ code=self.code,
57
+ message=self.message,
58
+ details=self.details,
59
+ hint=self.hint,
60
+ )
61
+
62
+
63
+ class HwpxHandleNotFoundError(HwpxOperationError):
64
+ """등록되지 않은 핸들 조회 시 사용하는 예외."""
65
+
66
+
67
+ class HwpxOps:
68
+ """MCP 도구에서 활용하는 안전한 고수준 헬퍼 모음."""
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ base_directory: Path | None = None,
74
+ paging_paragraph_limit: int = DEFAULT_PAGING_PARAGRAPH_LIMIT,
75
+ auto_backup: bool = False,
76
+ storage: DocumentStorage | None = None,
77
+ ) -> None:
78
+ if storage is not None and (base_directory is not None or auto_backup):
79
+ logger.debug(
80
+ "Ignoring base_directory/auto_backup parameters because explicit storage was provided",
81
+ extra={
82
+ "base_directory": str(base_directory) if base_directory else None
83
+ },
84
+ )
85
+
86
+ if storage is None:
87
+ storage = LocalDocumentStorage(
88
+ base_directory=base_directory,
89
+ auto_backup=auto_backup,
90
+ logger=logger,
91
+ )
92
+
93
+ context = DocumentContext(
94
+ storage=storage,
95
+ paging_paragraph_limit=paging_paragraph_limit,
96
+ error_type=HwpxOperationError,
97
+ handle_error_type=HwpxHandleNotFoundError,
98
+ )
99
+ self._services: HwpxOpsServices = build_hwpx_ops_services(context)
100
+ self.storage = storage
101
+ self.base_directory = storage.base_directory
102
+ self.paging_limit = context.paging_limit
103
+ self._plan_manager = self._services.planning.plan_manager
104
+ self._registered_handles = context.registered_handles
105
+
106
+ @property
107
+ def plan_manager(self) -> PlanManager:
108
+ return self._services.planning.plan_manager
109
+
110
+ def _new_error(
111
+ self,
112
+ code: str,
113
+ message: str,
114
+ *,
115
+ details: Optional[Dict[str, Any]] = None,
116
+ hint: Optional[str] = None,
117
+ ) -> HwpxOperationError:
118
+ return cast(HwpxOperationError, self._services.context._new_error(
119
+ code, message, details=details, hint=hint
120
+ ))
121
+
122
+ def _resolve_path(self, path: str, *, must_exist: bool = True) -> Path:
123
+ return self._services.context._resolve_path(path, must_exist=must_exist)
124
+
125
+ def _make_handle_id(self, path: str, backend: Optional[str] = None) -> str:
126
+ return self._services.context._make_handle_id(path, backend)
127
+
128
+ def _register_handle(self, path: str, resolved: Path) -> RegisteredHandle:
129
+ return self._services.context._register_handle(path, resolved)
130
+
131
+ def list_registered_handles(self) -> List[RegisteredHandle]:
132
+ return self._services.context.list_registered_handles()
133
+
134
+ def open_document_handle(self, path: str) -> Dict[str, Any]:
135
+ return self._services.context.open_document_handle(path)
136
+
137
+ def list_open_documents(self) -> Dict[str, Any]:
138
+ return self._services.context.list_open_documents()
139
+
140
+ def close_document_handle(self, handle_id: str) -> Dict[str, Any]:
141
+ return self._services.context.close_document_handle(handle_id)
142
+
143
+ def get_registered_handle(self, handle_id: str) -> RegisteredHandle:
144
+ return self._services.context.get_registered_handle(handle_id)
145
+
146
+ def resolve_document_path(
147
+ self,
148
+ *,
149
+ path: Optional[str] = None,
150
+ handle_id: Optional[str] = None,
151
+ ) -> str:
152
+ return self._services.context.resolve_document_path(
153
+ path=path, handle_id=handle_id
154
+ )
155
+
156
+ def get_metadata_by_handle(self, handle_id: str) -> Dict[str, Any]:
157
+ return self._services.read_query.get_metadata_by_handle(handle_id)
158
+
159
+ def get_paragraphs_by_handle(self, handle_id: str) -> Dict[str, Any]:
160
+ return self._services.read_query.get_paragraphs_by_handle(handle_id)
161
+
162
+ def get_tables_by_handle(self, handle_id: str) -> Dict[str, Any]:
163
+ return self._services.read_query.get_tables_by_handle(handle_id)
164
+
165
+ def _resolve_output_path(self, path: str) -> Path:
166
+ return self._services.context._resolve_output_path(path)
167
+
168
+ def _ensure_backup(self, path: Path) -> Optional[Path]:
169
+ return self._services.save._ensure_backup(path)
170
+
171
+ def _relative_path(self, path: Path) -> str:
172
+ return self._services.context._relative_path(path)
173
+
174
+ def _maybe_backup(self, path: Path) -> None:
175
+ return self._services.save._maybe_backup(path)
176
+
177
+ def _open_document(self, path: str) -> Tuple[HwpxDocument, Path]:
178
+ return self._services.context._open_document(path)
179
+
180
+ def _read_only_hwp_paragraphs(self, path: str) -> Tuple[List[str], Path, str]:
181
+ return self._services.context._read_only_hwp_paragraphs(path)
182
+
183
+ def _ensure_planner_document(self, doc_id: str, path: str) -> None:
184
+ return self._services.planning._ensure_planner_document(doc_id, path)
185
+
186
+ def _save_document(
187
+ self, document: HwpxDocument, target: Path, *, quality: Any = None
188
+ ) -> Dict[str, Any]:
189
+ return self._services.save._save_document(document, target, quality=quality)
190
+
191
+ def _save_transaction_document(
192
+ self, document: HwpxDocument, target: Path, *, quality: Any = None
193
+ ) -> Dict[str, Any]:
194
+ return self._services.save._save_transaction_document(
195
+ document, target, quality=quality
196
+ )
197
+
198
+ @staticmethod
199
+ def _report_for_bytes(data: bytes, *, file_path: Path) -> Dict[str, Any]:
200
+ return SavePolicy._report_for_bytes(data, file_path=file_path)
201
+
202
+ @staticmethod
203
+ def _semantic_diff_bytes(before: bytes, after: bytes) -> Dict[str, Any]:
204
+ return SavePolicy._semantic_diff_bytes(before, after)
205
+
206
+ def _capture_exact_sidecar_guard(self, path: Path) -> WorkspaceOutputGuard:
207
+ """Authorize one derived sidecar name without following a final alias."""
208
+ return self._services.save._capture_exact_sidecar_guard(path)
209
+
210
+ @staticmethod
211
+ def _absent_publication_guard(
212
+ guard: WorkspaceOutputGuard,
213
+ ) -> WorkspaceOutputGuard:
214
+ """Represent the exact absent state produced by a guarded deletion."""
215
+ return SavePolicy._absent_publication_guard(guard)
216
+
217
+ def _assert_exact_sidecar_publication(
218
+ self,
219
+ guard: WorkspaceOutputGuard,
220
+ ) -> None:
221
+ """Revalidate either an exact file publication or guarded absence."""
222
+ return self._services.save._assert_exact_sidecar_publication(guard)
223
+
224
+ def _publish_exact_recovery(
225
+ self,
226
+ base_path: Path,
227
+ data: bytes,
228
+ *,
229
+ mode: int | None,
230
+ marker: str,
231
+ max_candidates: int = 32,
232
+ ) -> _ExactRecoveryPublication:
233
+ """Publish recovery bytes without overwriting an existing sidecar."""
234
+ return self._services.save._publish_exact_recovery(
235
+ base_path, data, mode=mode, marker=marker, max_candidates=max_candidates
236
+ )
237
+
238
+ def _preserve_exact_preimages(
239
+ self,
240
+ preimages: Sequence[tuple[Path, bytes, int | None]],
241
+ *,
242
+ marker: str,
243
+ ) -> tuple[_ExactRecoveryPublication, ...] | None:
244
+ """Preserve every preimage before any destructive mutation begins."""
245
+ return self._services.save._preserve_exact_preimages(preimages, marker=marker)
246
+
247
+ def _cleanup_exact_recoveries(
248
+ self,
249
+ recoveries: Sequence[_ExactRecoveryPublication],
250
+ ) -> tuple[bool, bool]:
251
+ """Remove proven recoveries, republishing all if any cleanup loses CAS."""
252
+ return self._services.save._cleanup_exact_recoveries(recoveries)
253
+
254
+ def _republish_exact_recoveries(
255
+ self,
256
+ recoveries: Sequence[_ExactRecoveryPublication],
257
+ ) -> bool:
258
+ """Recreate immutable recovery copies after cleanup or claim loss."""
259
+ return self._services.save._republish_exact_recoveries(recoveries)
260
+
261
+ def _rotate_and_backup_exact(
262
+ self,
263
+ target: Path,
264
+ *,
265
+ target_guard: WorkspaceOutputGuard | None = None,
266
+ target_bytes: bytes | None = None,
267
+ max_backups: int = 5,
268
+ ) -> _ExactBackupResult:
269
+ """Rotate local sidecars from no-follow guards and an exact preimage."""
270
+ return self._services.save._rotate_and_backup_exact(
271
+ target,
272
+ target_guard=target_guard,
273
+ target_bytes=target_bytes,
274
+ max_backups=max_backups,
275
+ )
276
+
277
+ def _rollback_exact_backup_mutations(
278
+ self,
279
+ mutations: Sequence[_ExactSidecarMutation],
280
+ *,
281
+ preimages_preserved: bool = False,
282
+ ) -> None:
283
+ """Restore every sidecar candidate that is still exactly ours."""
284
+ return self._services.save._rollback_exact_backup_mutations(
285
+ mutations, preimages_preserved=preimages_preserved
286
+ )
287
+
288
+ def _decode_image_base64(self, image_base64: str) -> bytes:
289
+ return self._services.media._decode_image_base64(image_base64)
290
+
291
+ def _id_integrity_payload(self, document: HwpxDocument) -> Dict[str, Any]:
292
+ return self._services.media._id_integrity_payload(document)
293
+
294
+ def _with_transaction_verification(
295
+ self,
296
+ result: Dict[str, Any],
297
+ document: HwpxDocument,
298
+ target: Path,
299
+ *,
300
+ dry_run: bool,
301
+ quality: Any = None,
302
+ ) -> Dict[str, Any]:
303
+ return self._services.transactions._with_transaction_verification(
304
+ result, document, target, dry_run=dry_run, quality=quality
305
+ )
306
+
307
+ def _operation_value(
308
+ self, operation: Dict[str, Any], *names: str, default: Any = None
309
+ ) -> Any:
310
+ return self._services.transactions._operation_value(
311
+ operation, *names, default=default
312
+ )
313
+
314
+ def _apply_transaction_operation(
315
+ self,
316
+ document: HwpxDocument,
317
+ operation: Dict[str, Any],
318
+ index: int,
319
+ ) -> Dict[str, Any]:
320
+ return self._services.transactions._apply_transaction_operation(
321
+ document, operation, index
322
+ )
323
+
324
+ def apply_edits(
325
+ self,
326
+ path: str,
327
+ operations: Sequence[Dict[str, Any]],
328
+ *,
329
+ dry_run: bool = False,
330
+ quality: Any = None,
331
+ ) -> Dict[str, Any]:
332
+ return self._services.transactions.apply_edits(
333
+ path, operations, dry_run=dry_run, quality=quality
334
+ )
335
+
336
+ def undo_last_edit(self, path: str) -> Dict[str, Any]:
337
+ return self._services.transactions.undo_last_edit(path)
338
+
339
+ def _iter_paragraphs(self, document: HwpxDocument) -> List[HwpxOxmlParagraph]:
340
+ return self._services.context._iter_paragraphs(document)
341
+
342
+ def _iter_tables(self, document: HwpxDocument) -> List[HwpxOxmlTable]:
343
+ return self._services.context._iter_tables(document)
344
+
345
+ def _auto_fit_table_columns(self, table: HwpxOxmlTable) -> List[int]:
346
+ return self._services.tables._auto_fit_table_columns(table)
347
+
348
+ def _normalize_color(self, color: str | None) -> Optional[str]:
349
+ return self._services.memo_style._normalize_color(color)
350
+
351
+ def _ensure_char_style(
352
+ self,
353
+ document: HwpxDocument,
354
+ run_style: Optional[Dict[str, Any]],
355
+ ) -> Optional[str]:
356
+ return self._services.memo_style._ensure_char_style(document, run_style)
357
+
358
+ def _ensure_table_border_fill(
359
+ self,
360
+ document: HwpxDocument,
361
+ *,
362
+ border_style: Optional[str] = None,
363
+ border_color: Optional[str] = None,
364
+ border_width: Optional[str | float | int] = None,
365
+ fill_color: Optional[str] = None,
366
+ ) -> str:
367
+ return self._services.tables._ensure_table_border_fill(
368
+ document,
369
+ border_style=border_style,
370
+ border_color=border_color,
371
+ border_width=border_width,
372
+ fill_color=fill_color,
373
+ )
374
+
375
+ def open_info(self, path: str) -> Dict[str, Any]:
376
+ return self._services.read_query.open_info(path)
377
+
378
+ def list_sections(self, path: str) -> Dict[str, Any]:
379
+ return self._services.read_query.list_sections(path)
380
+
381
+ def list_headers(self, path: str) -> Dict[str, Any]:
382
+ return self._services.read_query.list_headers(path)
383
+
384
+ def package_parts(self, path: str) -> Dict[str, Any]:
385
+ return self._services.package_validation.package_parts(path)
386
+
387
+ def package_get_text(
388
+ self, path: str, part_name: str, encoding: str | None = None
389
+ ) -> Dict[str, Any]:
390
+ return self._services.package_validation.package_get_text(
391
+ path, part_name, encoding
392
+ )
393
+
394
+ def repair_hwpx(
395
+ self,
396
+ source: str,
397
+ output: str,
398
+ *,
399
+ recover: bool = False,
400
+ overwrite: bool = False,
401
+ max_entry_size: int = 64 * 1024 * 1024,
402
+ max_total_size: int = 512 * 1024 * 1024,
403
+ max_source_size: int = 512 * 1024 * 1024,
404
+ ) -> Dict[str, Any]:
405
+ return self._services.package_validation.repair_hwpx(
406
+ source,
407
+ output,
408
+ recover=recover,
409
+ overwrite=overwrite,
410
+ max_entry_size=max_entry_size,
411
+ max_total_size=max_total_size,
412
+ max_source_size=max_source_size,
413
+ )
414
+
415
+ def read_text(
416
+ self,
417
+ path: str,
418
+ *,
419
+ offset: int = 0,
420
+ limit: Optional[int] = None,
421
+ with_highlights: bool = False,
422
+ with_footnotes: bool = False,
423
+ ) -> Dict[str, Any]:
424
+ return self._services.read_query.read_text(
425
+ path,
426
+ offset=offset,
427
+ limit=limit,
428
+ with_highlights=with_highlights,
429
+ with_footnotes=with_footnotes,
430
+ )
431
+
432
+ def get_paragraphs(
433
+ self,
434
+ path: str,
435
+ paragraph_indexes: Sequence[int],
436
+ *,
437
+ with_highlights: bool = False,
438
+ with_footnotes: bool = False,
439
+ ) -> Dict[str, Any]:
440
+ return self._services.read_query.get_paragraphs(
441
+ path,
442
+ paragraph_indexes,
443
+ with_highlights=with_highlights,
444
+ with_footnotes=with_footnotes,
445
+ )
446
+
447
+ def text_extract_report(self, path: str, mode: str = "plain") -> Dict[str, Any]:
448
+ return self._services.read_query.text_extract_report(path, mode)
449
+
450
+ def analyze_template_structure(
451
+ self,
452
+ path: str,
453
+ *,
454
+ placeholder_patterns: Optional[Sequence[str]] = None,
455
+ lock_keywords: Optional[Sequence[str]] = None,
456
+ ) -> Dict[str, Any]:
457
+ return self._services.read_query.analyze_template_structure(
458
+ path, placeholder_patterns=placeholder_patterns, lock_keywords=lock_keywords
459
+ )
460
+
461
+ def find(
462
+ self,
463
+ path: str,
464
+ query: str,
465
+ *,
466
+ is_regex: bool = False,
467
+ max_results: int = 100,
468
+ context_radius: int = 80,
469
+ ) -> Dict[str, Any]:
470
+ return self._services.read_query.find(
471
+ path,
472
+ query,
473
+ is_regex=is_regex,
474
+ max_results=max_results,
475
+ context_radius=context_radius,
476
+ )
477
+
478
+ def find_runs_by_style(
479
+ self,
480
+ path: str,
481
+ *,
482
+ filters: Optional[Dict[str, Any]] = None,
483
+ max_results: int = 200,
484
+ ) -> Dict[str, Any]:
485
+ return self._services.memo_style.find_runs_by_style(
486
+ path, filters=filters, max_results=max_results
487
+ )
488
+
489
+ def replace_text_in_runs(
490
+ self,
491
+ path: str,
492
+ search: str,
493
+ replacement: str,
494
+ *,
495
+ style_filter: Optional[Dict[str, Any]] = None,
496
+ limit_per_run: Optional[int] = None,
497
+ dry_run: bool = False,
498
+ ) -> Dict[str, Any]:
499
+ return self._services.content_layout.replace_text_in_runs(
500
+ path,
501
+ search,
502
+ replacement,
503
+ style_filter=style_filter,
504
+ limit_per_run=limit_per_run,
505
+ dry_run=dry_run,
506
+ )
507
+
508
+ def add_paragraph(
509
+ self,
510
+ path: str,
511
+ text: str = "",
512
+ *,
513
+ section_index: Optional[int] = None,
514
+ run_style: Optional[Dict[str, Any]] = None,
515
+ ) -> Dict[str, Any]:
516
+ return self._services.content_layout.add_paragraph(
517
+ path, text, section_index=section_index, run_style=run_style
518
+ )
519
+
520
+ def insert_paragraphs_bulk(
521
+ self,
522
+ path: str,
523
+ paragraphs: Sequence[str],
524
+ *,
525
+ section_index: Optional[int] = None,
526
+ run_style: Optional[Dict[str, Any]] = None,
527
+ dry_run: bool = False,
528
+ ) -> Dict[str, Any]:
529
+ return self._services.content_layout.insert_paragraphs_bulk(
530
+ path,
531
+ paragraphs,
532
+ section_index=section_index,
533
+ run_style=run_style,
534
+ dry_run=dry_run,
535
+ )
536
+
537
+ def set_paragraph_format(
538
+ self,
539
+ path: str,
540
+ *,
541
+ paragraph_index: Optional[int] = None,
542
+ paragraph_indexes: Optional[Sequence[int]] = None,
543
+ alignment: Optional[str] = None,
544
+ line_spacing_percent: Optional[float] = None,
545
+ indent_left_mm: Optional[float] = None,
546
+ indent_right_mm: Optional[float] = None,
547
+ first_line_indent_mm: Optional[float] = None,
548
+ spacing_before_pt: Optional[float] = None,
549
+ spacing_after_pt: Optional[float] = None,
550
+ outline_level: Optional[int] = None,
551
+ keep_with_next: Optional[bool] = None,
552
+ keep_lines: Optional[bool] = None,
553
+ page_break_before: Optional[bool] = None,
554
+ dry_run: bool = False,
555
+ ) -> Dict[str, Any]:
556
+ return self._services.content_layout.set_paragraph_format(
557
+ path,
558
+ paragraph_index=paragraph_index,
559
+ paragraph_indexes=paragraph_indexes,
560
+ alignment=alignment,
561
+ line_spacing_percent=line_spacing_percent,
562
+ indent_left_mm=indent_left_mm,
563
+ indent_right_mm=indent_right_mm,
564
+ first_line_indent_mm=first_line_indent_mm,
565
+ spacing_before_pt=spacing_before_pt,
566
+ spacing_after_pt=spacing_after_pt,
567
+ outline_level=outline_level,
568
+ keep_with_next=keep_with_next,
569
+ keep_lines=keep_lines,
570
+ page_break_before=page_break_before,
571
+ dry_run=dry_run,
572
+ )
573
+
574
+ def set_page_setup(
575
+ self,
576
+ path: str,
577
+ *,
578
+ paper_size: Optional[str] = None,
579
+ width_mm: Optional[float] = None,
580
+ height_mm: Optional[float] = None,
581
+ orientation: Optional[str] = None,
582
+ margins_mm: Optional[Dict[str, float]] = None,
583
+ margin_left_mm: Optional[float] = None,
584
+ margin_right_mm: Optional[float] = None,
585
+ margin_top_mm: Optional[float] = None,
586
+ margin_bottom_mm: Optional[float] = None,
587
+ header_margin_mm: Optional[float] = None,
588
+ footer_margin_mm: Optional[float] = None,
589
+ gutter_mm: Optional[float] = None,
590
+ columns: Optional[int] = None,
591
+ column_gap_mm: Optional[float] = None,
592
+ section_index: Optional[int] = None,
593
+ dry_run: bool = False,
594
+ ) -> Dict[str, Any]:
595
+ return self._services.content_layout.set_page_setup(
596
+ path,
597
+ paper_size=paper_size,
598
+ width_mm=width_mm,
599
+ height_mm=height_mm,
600
+ orientation=orientation,
601
+ margins_mm=margins_mm,
602
+ margin_left_mm=margin_left_mm,
603
+ margin_right_mm=margin_right_mm,
604
+ margin_top_mm=margin_top_mm,
605
+ margin_bottom_mm=margin_bottom_mm,
606
+ header_margin_mm=header_margin_mm,
607
+ footer_margin_mm=footer_margin_mm,
608
+ gutter_mm=gutter_mm,
609
+ columns=columns,
610
+ column_gap_mm=column_gap_mm,
611
+ section_index=section_index,
612
+ dry_run=dry_run,
613
+ )
614
+
615
+ def _header_footer_payload(
616
+ self,
617
+ wrapper: Any,
618
+ *,
619
+ kind: str,
620
+ page_type: str,
621
+ ) -> Dict[str, Any]:
622
+ return self._services.content_layout._header_footer_payload(
623
+ wrapper, kind=kind, page_type=page_type
624
+ )
625
+
626
+ def set_header_footer(
627
+ self,
628
+ path: str,
629
+ *,
630
+ kind: str,
631
+ text: Optional[str] = None,
632
+ content: Optional[Sequence[Dict[str, Any]]] = None,
633
+ section_index: Optional[int] = None,
634
+ page_type: str = "BOTH",
635
+ dry_run: bool = False,
636
+ ) -> Dict[str, Any]:
637
+ return self._services.content_layout.set_header_footer(
638
+ path,
639
+ kind=kind,
640
+ text=text,
641
+ content=content,
642
+ section_index=section_index,
643
+ page_type=page_type,
644
+ dry_run=dry_run,
645
+ )
646
+
647
+ def set_page_number(
648
+ self,
649
+ path: str,
650
+ *,
651
+ target: str = "footer",
652
+ page_type: str = "BOTH",
653
+ format: str = "page",
654
+ align: str = "CENTER",
655
+ position: str = "BOTTOM_CENTER",
656
+ prefix: str = "",
657
+ suffix: str = "",
658
+ format_type: Optional[str] = None,
659
+ section_index: Optional[int] = None,
660
+ dry_run: bool = False,
661
+ ) -> Dict[str, Any]:
662
+ return self._services.content_layout.set_page_number(
663
+ path,
664
+ target=target,
665
+ page_type=page_type,
666
+ format=format,
667
+ align=align,
668
+ position=position,
669
+ prefix=prefix,
670
+ suffix=suffix,
671
+ format_type=format_type,
672
+ section_index=section_index,
673
+ dry_run=dry_run,
674
+ )
675
+
676
+ def set_list_format(
677
+ self,
678
+ path: str,
679
+ *,
680
+ paragraph_index: Optional[int] = None,
681
+ paragraph_indexes: Optional[Sequence[int]] = None,
682
+ kind: str = "bullet",
683
+ level: int = 1,
684
+ bullet_char: Optional[str] = None,
685
+ number_format: Optional[str] = None,
686
+ start: Optional[int] = None,
687
+ dry_run: bool = False,
688
+ ) -> Dict[str, Any]:
689
+ return self._services.content_layout.set_list_format(
690
+ path,
691
+ paragraph_index=paragraph_index,
692
+ paragraph_indexes=paragraph_indexes,
693
+ kind=kind,
694
+ level=level,
695
+ bullet_char=bullet_char,
696
+ number_format=number_format,
697
+ start=start,
698
+ dry_run=dry_run,
699
+ )
700
+
701
+ def list_form_fields(
702
+ self,
703
+ path: str,
704
+ ) -> Dict[str, Any]:
705
+ return self._services.form_fields.list_form_fields(path)
706
+
707
+ def fill_form_field(
708
+ self,
709
+ path: str,
710
+ *,
711
+ value: str,
712
+ field_index: Optional[int] = None,
713
+ field_id: Optional[str] = None,
714
+ name: Optional[str] = None,
715
+ dry_run: bool = False,
716
+ ) -> Dict[str, Any]:
717
+ return self._services.form_fields.fill_form_field(
718
+ path,
719
+ value=value,
720
+ field_index=field_index,
721
+ field_id=field_id,
722
+ name=name,
723
+ dry_run=dry_run,
724
+ )
725
+
726
+ def add_table(
727
+ self,
728
+ path: str,
729
+ rows: int,
730
+ cols: int,
731
+ *,
732
+ section_index: Optional[int] = None,
733
+ border_style: str | None = None,
734
+ border_color: Optional[str] = None,
735
+ border_width: Optional[str | float | int] = None,
736
+ fill_color: Optional[str] = None,
737
+ auto_fit: bool = False,
738
+ ) -> Dict[str, Any]:
739
+ return self._services.tables.add_table(
740
+ path,
741
+ rows,
742
+ cols,
743
+ section_index=section_index,
744
+ border_style=border_style,
745
+ border_color=border_color,
746
+ border_width=border_width,
747
+ fill_color=fill_color,
748
+ auto_fit=auto_fit,
749
+ )
750
+
751
+ def set_table_border_fill(
752
+ self,
753
+ path: str,
754
+ table_index: int,
755
+ *,
756
+ border_style: str | None = None,
757
+ border_color: Optional[str] = None,
758
+ border_width: Optional[str | float | int] = None,
759
+ fill_color: Optional[str] = None,
760
+ ) -> Dict[str, Any]:
761
+ return self._services.tables.set_table_border_fill(
762
+ path,
763
+ table_index,
764
+ border_style=border_style,
765
+ border_color=border_color,
766
+ border_width=border_width,
767
+ fill_color=fill_color,
768
+ )
769
+
770
+ def get_table_cell_map(
771
+ self,
772
+ path: str,
773
+ table_index: int,
774
+ ) -> Dict[str, Any]:
775
+ return self._services.tables.get_table_cell_map(path, table_index)
776
+
777
+ def set_table_cell_text(
778
+ self,
779
+ path: str,
780
+ table_index: int,
781
+ row: int,
782
+ col: int,
783
+ text: str,
784
+ *,
785
+ dry_run: bool = False,
786
+ logical: Optional[bool] = None,
787
+ split_merged: Optional[bool] = None,
788
+ auto_fit: bool = False,
789
+ ) -> Dict[str, Any]:
790
+ return self._services.tables.set_table_cell_text(
791
+ path,
792
+ table_index,
793
+ row,
794
+ col,
795
+ text,
796
+ dry_run=dry_run,
797
+ logical=logical,
798
+ split_merged=split_merged,
799
+ auto_fit=auto_fit,
800
+ )
801
+
802
+ def replace_table_region(
803
+ self,
804
+ path: str,
805
+ table_index: int,
806
+ start_row: int,
807
+ start_col: int,
808
+ values: Sequence[Sequence[str]],
809
+ *,
810
+ dry_run: bool = False,
811
+ logical: Optional[bool] = None,
812
+ split_merged: Optional[bool] = None,
813
+ auto_fit: bool = False,
814
+ ) -> Dict[str, Any]:
815
+ return self._services.tables.replace_table_region(
816
+ path,
817
+ table_index,
818
+ start_row,
819
+ start_col,
820
+ values,
821
+ dry_run=dry_run,
822
+ logical=logical,
823
+ split_merged=split_merged,
824
+ auto_fit=auto_fit,
825
+ )
826
+
827
+ def byte_preserving_patch(
828
+ self,
829
+ path: str,
830
+ patches: Sequence[Dict[str, Any]],
831
+ *,
832
+ output: Optional[str] = None,
833
+ ) -> Dict[str, Any]:
834
+ return self._services.transactions.byte_preserving_patch(
835
+ path, patches, output=output
836
+ )
837
+
838
+ def _write_patched(
839
+ self,
840
+ target_path,
841
+ data: bytes,
842
+ payload: Dict[str, Any],
843
+ *,
844
+ output_guard: (
845
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
846
+ ) = None,
847
+ output_precondition: (
848
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
849
+ ) = None,
850
+ publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
851
+ ) -> Dict[str, Any]:
852
+ """Atomic temp-write + open-safety gate for a byte-preserving result
853
+ (shared by byte_preserving_patch / apply_table_ops)."""
854
+ return self._services.save._write_patched(
855
+ target_path,
856
+ data,
857
+ payload,
858
+ output_guard=output_guard,
859
+ output_precondition=output_precondition,
860
+ publication_sink=publication_sink,
861
+ )
862
+
863
+ def apply_table_ops(
864
+ self,
865
+ path: str,
866
+ ops: Sequence[Dict[str, Any]],
867
+ *,
868
+ output: Optional[str] = None,
869
+ render_check: str = "off",
870
+ dry_run: bool = False,
871
+ output_guard: (
872
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
873
+ ) = None,
874
+ publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
875
+ ) -> Dict[str, Any]:
876
+ """Byte-preserving structural form-fill: apply cell fills + table structure
877
+ ops (delete_column/row/table, insert_row_by_clone, insert_block_by_clone)
878
+ preserving every untouched byte. Cells/tables may be addressed by
879
+ tableAnchor/cellAnchor (unique-or-skip) as well as index. Optional
880
+ real-Hancom render gate.
881
+
882
+ dryRun=true: identical pipeline (resolution/validation/fail-closed all
883
+ real) but writes NOTHING — returns transcript (per-op resolution + before/
884
+ after dims) and applied old→new texts as approval evidence for the user
885
+ consult loop. renderCheck still works on the would-be bytes."""
886
+ return self._services.form_fields.apply_table_ops(
887
+ path,
888
+ ops,
889
+ output=output,
890
+ render_check=render_check,
891
+ dry_run=dry_run,
892
+ output_guard=output_guard,
893
+ publication_sink=publication_sink,
894
+ )
895
+
896
+ def verify_form_fill(
897
+ self,
898
+ path: str,
899
+ before_path: str,
900
+ *,
901
+ require: bool = False,
902
+ ) -> Dict[str, Any]:
903
+ """Render before/after in REAL Hancom and judge overflow/overlap/layout.
904
+ Honest degrade (renderChecked=false) with no oracle unless require=true."""
905
+ return self._services.form_fields.verify_form_fill(
906
+ path, before_path, require=require
907
+ )
908
+
909
+ def score_form_fill(
910
+ self,
911
+ path: str,
912
+ gold_path: str,
913
+ blank_path: str,
914
+ *,
915
+ run_render: bool = True,
916
+ expected_pages: Optional[int] = None,
917
+ ) -> Dict[str, Any]:
918
+ """Score a filled evaluation-plan form against a gold reference on 5 axes
919
+ (A render cleanliness / B byte fidelity vs blank / C structure vs gold /
920
+ D content completeness / E compliance) -> weighted 0-100 + per-axis gap
921
+ report. The fitness function of the form-fill quality loop.
922
+
923
+ ``path`` = produced fill, ``goldPath`` = accepted reference of the same
924
+ form family, ``blankPath`` = the empty province form. A requires a real
925
+ Hancom render (renderCheck); with no oracle A is ``unverified`` (never a
926
+ silent pass). Set runRender=false for a fast structural-only pass."""
927
+ return self._services.form_fields.score_form_fill(
928
+ path,
929
+ gold_path,
930
+ blank_path,
931
+ run_render=run_render,
932
+ expected_pages=expected_pages,
933
+ )
934
+
935
+ def apply_body_ops(
936
+ self,
937
+ path: str,
938
+ ops: Sequence[Dict[str, Any]],
939
+ *,
940
+ output: Optional[str] = None,
941
+ dry_run: bool = False,
942
+ output_guard: (
943
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
944
+ ) = None,
945
+ publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
946
+ ) -> Dict[str, Any]:
947
+ """Byte-preserving BODY(표 밖 직속 문단) ops — Stage 2 결정표의 본문 어휘.
948
+
949
+ ops: replace_text{find,replace,count=1: <hp:t> 텍스트 안에서만, 개수 불일치
950
+ refuse} · delete_paragraph{index: 표 품은 문단 refuse} ·
951
+ insert_paragraph_by_clone{ref_index,count,texts?: 참조 문단 서식 verbatim
952
+ 상속} · reorder_paragraphs{start,end,order}. index는 op 실행 시점 기준.
953
+ dryRun=true면 아무것도 쓰지 않고 transcript만(승인 근거)."""
954
+ return self._services.form_fields.apply_body_ops(
955
+ path,
956
+ ops,
957
+ output=output,
958
+ dry_run=dry_run,
959
+ output_guard=output_guard,
960
+ publication_sink=publication_sink,
961
+ )
962
+
963
+ def inspect_fill_residue(
964
+ self,
965
+ path: str,
966
+ *,
967
+ blank_path: Optional[str] = None,
968
+ ) -> Dict[str, Any]:
969
+ """채움본 잔존물 zero-체크(비변형) — 제출 전 기계 게이트(Stage 3).
970
+
971
+ blank를 주면 범례 기반 신호까지: 삭제색 잔존·미수정 샘플(수정색이 blank와
972
+ 동일=prose 샘플 미교체) = ERROR. placeholder ◯◯◯/□□□=ERROR, **=각주와
973
+ 중의적이라 needs_review, 고아 마커=needs_review. ok=true는 필요조건일 뿐 —
974
+ 제출 확언은 렌더 PDF를 사람이 전 페이지 본 뒤에만."""
975
+ return self._services.form_fields.inspect_fill_residue(
976
+ path, blank_path=blank_path
977
+ )
978
+
979
+ def scan_form_guidance(self, path: str, *, max_items: int = 60) -> Dict[str, Any]:
980
+ """Recon an unfamiliar form (NON-MUTATING) — universal form-fill Stage 1.
981
+
982
+ Walks every run INCLUDING table-cell interiors and table captions, parses
983
+ the form's own colour legend (e.g. "검정 유지/파랑 수정/빨강 삭제"), and
984
+ reports candidates with table_patch-compatible addresses: delete candidates
985
+ (legend-delete colour + guidance keywords), modify targets, placeholder
986
+ tokens (◯◯◯/**/□□□), conditional-choice blocks, empty cells with neighbour
987
+ label + charPr format context, and an honest question list. Candidates are
988
+ proposals — destructive ops still require user approval."""
989
+ return self._services.form_fields.scan_form_guidance(path, max_items=max_items)
990
+
991
+ def apply_evalplan_fill(
992
+ self,
993
+ path: str,
994
+ review_md: str,
995
+ *,
996
+ phase: str = "all",
997
+ output: Optional[str] = None,
998
+ render_check: str = "off",
999
+ score_gold_path: Optional[str] = None,
1000
+ expected_pages: Optional[int] = None,
1001
+ output_guard: (
1002
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
1003
+ ) = None,
1004
+ publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
1005
+ ) -> Dict[str, Any]:
1006
+ """Whole-form 평가계획 fill: {blank province form + review markdown} ->
1007
+ byte-preserving gold-quality 채움본 in ONE call. Runs the structure-driven
1008
+ recipe (delete red/optional tables + 정기시험 column, fill 운영계획/성취기준/
1009
+ 성취수준/성취율/반영비율/rubrics incl. 채점기준 ladder/prose sections),
1010
+ preserving the blank's formatting (never regenerates). Works on BOTH the
1011
+ 2015-개정 and 2022-개정 form families (auto-detected from the blank + review).
1012
+
1013
+ ``path`` = blank form, ``reviewMd`` = the structured review markdown
1014
+ (Ⅰ 운영계획 + [1]~[11]). ``phase`` = ``"structural"`` | ``"all"`` (default) |
1015
+ ``"clean"``; ``"clean"`` additionally runs core's deterministic post-fill
1016
+ cleanup so a submittable 채움본 comes back in one call (report under
1017
+ ``contentReport.finalize``). Returns the produced path + per-region
1018
+ contentReport with rubricNeedsReview (honest-defer count, never silent). Set
1019
+ renderCheck='required' to gate on a real Hancom render; pass scoreGoldPath
1020
+ (an accepted form of the same family) to also return the 5-axis scorecard."""
1021
+ return self._services.form_fields.apply_evalplan_fill(
1022
+ path,
1023
+ review_md,
1024
+ phase=phase,
1025
+ output=output,
1026
+ render_check=render_check,
1027
+ score_gold_path=score_gold_path,
1028
+ expected_pages=expected_pages,
1029
+ output_guard=output_guard,
1030
+ publication_sink=publication_sink,
1031
+ )
1032
+
1033
+ def split_table_cell(
1034
+ self,
1035
+ path: str,
1036
+ table_index: int,
1037
+ row: int,
1038
+ col: int,
1039
+ ) -> Dict[str, Any]:
1040
+ return self._services.tables.split_table_cell(path, table_index, row, col)
1041
+
1042
+ def copy_table_between_documents(
1043
+ self,
1044
+ source_path: str,
1045
+ source_table_index: int,
1046
+ target_path: str,
1047
+ *,
1048
+ target_section_index: Optional[int] = None,
1049
+ auto_fit: bool = False,
1050
+ ) -> Dict[str, Any]:
1051
+ return self._services.tables.copy_table_between_documents(
1052
+ source_path,
1053
+ source_table_index,
1054
+ target_path,
1055
+ target_section_index=target_section_index,
1056
+ auto_fit=auto_fit,
1057
+ )
1058
+
1059
+ def add_shape(
1060
+ self,
1061
+ path: str,
1062
+ *,
1063
+ shape_type: str = "RECTANGLE",
1064
+ section_index: Optional[int] = None,
1065
+ dry_run: bool = True,
1066
+ ) -> Dict[str, Any]:
1067
+ return self._services.media.add_shape(
1068
+ path, shape_type=shape_type, section_index=section_index, dry_run=dry_run
1069
+ )
1070
+
1071
+ def add_control(
1072
+ self,
1073
+ path: str,
1074
+ *,
1075
+ control_type: str = "TEXTBOX",
1076
+ section_index: Optional[int] = None,
1077
+ dry_run: bool = True,
1078
+ ) -> Dict[str, Any]:
1079
+ return self._services.media.add_control(
1080
+ path,
1081
+ control_type=control_type,
1082
+ section_index=section_index,
1083
+ dry_run=dry_run,
1084
+ )
1085
+
1086
+ def insert_picture(
1087
+ self,
1088
+ path: str,
1089
+ image_base64: str,
1090
+ *,
1091
+ image_format: str = "png",
1092
+ width: Optional[int] = None,
1093
+ height: Optional[int] = None,
1094
+ width_mm: Optional[float] = None,
1095
+ height_mm: Optional[float] = None,
1096
+ section_index: Optional[int] = None,
1097
+ align: Optional[str] = None,
1098
+ output: Optional[str] = None,
1099
+ dry_run: bool = False,
1100
+ ) -> Dict[str, Any]:
1101
+ return self._services.media.insert_picture(
1102
+ path,
1103
+ image_base64,
1104
+ image_format=image_format,
1105
+ width=width,
1106
+ height=height,
1107
+ width_mm=width_mm,
1108
+ height_mm=height_mm,
1109
+ section_index=section_index,
1110
+ align=align,
1111
+ output=output,
1112
+ dry_run=dry_run,
1113
+ )
1114
+
1115
+ def replace_picture(
1116
+ self,
1117
+ path: str,
1118
+ image_base64: str,
1119
+ *,
1120
+ image_format: str = "png",
1121
+ picture_index: int = 0,
1122
+ binary_item_id_ref: Optional[str] = None,
1123
+ remove_orphaned: bool = True,
1124
+ output: Optional[str] = None,
1125
+ dry_run: bool = False,
1126
+ ) -> Dict[str, Any]:
1127
+ return self._services.media.replace_picture(
1128
+ path,
1129
+ image_base64,
1130
+ image_format=image_format,
1131
+ picture_index=picture_index,
1132
+ binary_item_id_ref=binary_item_id_ref,
1133
+ remove_orphaned=remove_orphaned,
1134
+ output=output,
1135
+ dry_run=dry_run,
1136
+ )
1137
+
1138
+ def add_memo(
1139
+ self,
1140
+ path: str,
1141
+ text: str,
1142
+ *,
1143
+ section_index: Optional[int] = None,
1144
+ author: str | None = None,
1145
+ timestamp: str | None = None,
1146
+ ) -> Dict[str, Any]:
1147
+ return self._services.memo_style.add_memo(
1148
+ path, text, section_index=section_index, author=author, timestamp=timestamp
1149
+ )
1150
+
1151
+ def attach_memo_field(
1152
+ self,
1153
+ path: str,
1154
+ paragraph_index: int,
1155
+ memo_id: str,
1156
+ ) -> Dict[str, Any]:
1157
+ return self._services.memo_style.attach_memo_field(
1158
+ path, paragraph_index, memo_id
1159
+ )
1160
+
1161
+ def add_memo_with_anchor(
1162
+ self,
1163
+ path: str,
1164
+ *,
1165
+ text: str,
1166
+ section_index: Optional[int] = None,
1167
+ memo_shape_id_ref: str | None = None,
1168
+ ) -> Dict[str, Any]:
1169
+ return self._services.memo_style.add_memo_with_anchor(
1170
+ path,
1171
+ text=text,
1172
+ section_index=section_index,
1173
+ memo_shape_id_ref=memo_shape_id_ref,
1174
+ )
1175
+
1176
+ def remove_memo(
1177
+ self,
1178
+ path: str,
1179
+ memo_id: str,
1180
+ *,
1181
+ dry_run: bool = True,
1182
+ ) -> Dict[str, Any]:
1183
+ return self._services.memo_style.remove_memo(path, memo_id, dry_run=dry_run)
1184
+
1185
+ def _find_memo(
1186
+ self, document: HwpxDocument, memo_id: str
1187
+ ) -> Optional[HwpxOxmlMemo]:
1188
+ return self._services.memo_style._find_memo(document, memo_id)
1189
+
1190
+ def ensure_run_style(self, path: str, **run_style: Any) -> Dict[str, Any]:
1191
+ return self._services.memo_style.ensure_run_style(path, **run_style)
1192
+
1193
+ def list_styles_and_bullets(self, path: str) -> Dict[str, Any]:
1194
+ return self._services.memo_style.list_styles_and_bullets(path)
1195
+
1196
+ def apply_style_to_text_ranges(
1197
+ self,
1198
+ path: str,
1199
+ spans: Sequence[Dict[str, int]],
1200
+ char_pr_id_ref: str,
1201
+ *,
1202
+ dry_run: bool = True,
1203
+ ) -> Dict[str, Any]:
1204
+ return self._services.memo_style.apply_style_to_text_ranges(
1205
+ path, spans, char_pr_id_ref, dry_run=dry_run
1206
+ )
1207
+
1208
+ def apply_style_to_paragraphs(
1209
+ self,
1210
+ path: str,
1211
+ paragraph_indexes: Sequence[int],
1212
+ char_pr_id_ref: str,
1213
+ *,
1214
+ dry_run: bool = True,
1215
+ ) -> Dict[str, Any]:
1216
+ return self._services.memo_style.apply_style_to_paragraphs(
1217
+ path, paragraph_indexes, char_pr_id_ref, dry_run=dry_run
1218
+ )
1219
+
1220
+ def save(self, path: str) -> Dict[str, Any]:
1221
+ return self._services.save.save(path)
1222
+
1223
+ def save_as(self, path: str, out: str) -> Dict[str, Any]:
1224
+ return self._services.save.save_as(path, out)
1225
+
1226
+ def fill_template(
1227
+ self,
1228
+ source: str,
1229
+ output: str,
1230
+ replacements: Dict[str, str],
1231
+ *,
1232
+ preserve_style: bool = True,
1233
+ split_newlines: bool = True,
1234
+ ) -> Dict[str, Any]:
1235
+ return self._services.save.fill_template(
1236
+ source,
1237
+ output,
1238
+ replacements,
1239
+ preserve_style=preserve_style,
1240
+ split_newlines=split_newlines,
1241
+ )
1242
+
1243
+ def export_text(self, path: str) -> Dict[str, Any]:
1244
+ """Export document content as plain text."""
1245
+ return self._services.preview_export.export_text(path)
1246
+
1247
+ def export_html(self, path: str) -> Dict[str, Any]:
1248
+ """Export document content as HTML."""
1249
+ return self._services.preview_export.export_html(path)
1250
+
1251
+ def export_markdown(self, path: str) -> Dict[str, Any]:
1252
+ """Export document content as Markdown."""
1253
+ return self._services.preview_export.export_markdown(path)
1254
+
1255
+ def _preview_output_dir(self, source_path: Path, output_dir: Optional[str]) -> Path:
1256
+ return self._services.preview_export._preview_output_dir(
1257
+ source_path, output_dir
1258
+ )
1259
+
1260
+ def _embed_screenshot_image(
1261
+ self,
1262
+ item: dict[str, Any],
1263
+ png_path: Path,
1264
+ *,
1265
+ embed_images: bool,
1266
+ max_image_bytes: int | None,
1267
+ ) -> None:
1268
+ """Attach a base64 PNG payload to a screenshot item, bounded by a byte cap.
1269
+
1270
+ Keeps the on-disk artifact regardless; only the inline payload is gated so
1271
+ an oversized page degrades to "path only" instead of bloating the response.
1272
+ """
1273
+ return self._services.preview_export._embed_screenshot_image(
1274
+ item, png_path, embed_images=embed_images, max_image_bytes=max_image_bytes
1275
+ )
1276
+
1277
+ def _capture_preview_pages(
1278
+ self,
1279
+ *,
1280
+ page_html_paths: Sequence[Path],
1281
+ pages: Sequence[Dict[str, Any]],
1282
+ output_dir: Path,
1283
+ max_pages: int | None,
1284
+ embed_images: bool = False,
1285
+ max_image_bytes: int | None = None,
1286
+ ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
1287
+ return self._services.preview_export._capture_preview_pages(
1288
+ page_html_paths=page_html_paths,
1289
+ pages=pages,
1290
+ output_dir=output_dir,
1291
+ max_pages=max_pages,
1292
+ embed_images=embed_images,
1293
+ max_image_bytes=max_image_bytes,
1294
+ )
1295
+
1296
+ def render_preview(
1297
+ self,
1298
+ path: str,
1299
+ output_dir: Optional[str] = None,
1300
+ mode: str = "pages",
1301
+ screenshot: str = "auto",
1302
+ max_pages: Optional[int] = None,
1303
+ embed_images: bool = False,
1304
+ max_image_bytes: Optional[int] = None,
1305
+ viewer: bool = False,
1306
+ ) -> Dict[str, Any]:
1307
+ """Generate layout-aware HTML and optional PNG preview artifacts.
1308
+
1309
+ When *embed_images* is true, each captured screenshot item also carries
1310
+ an ``imageBase64``/``imageMime`` payload (bounded by *max_image_bytes*,
1311
+ defaulting to ``_DEFAULT_MAX_PREVIEW_IMAGE_BYTES``) so a caller can return
1312
+ the page as an inline image content block.
1313
+
1314
+ When *viewer* is true, the manifest also carries a self-contained
1315
+ scrollable document viewer under ``viewer`` (equations as native MathML
1316
+ with ``python-hwpx[preview]``).
1317
+ """
1318
+ return self._services.preview_export.render_preview(
1319
+ path,
1320
+ output_dir,
1321
+ mode,
1322
+ screenshot,
1323
+ max_pages,
1324
+ embed_images,
1325
+ max_image_bytes,
1326
+ viewer,
1327
+ )
1328
+
1329
+ def make_blank(self, out: str) -> Dict[str, Any]:
1330
+ return self._services.save.make_blank(out)
1331
+
1332
+ def convert_hwp_to_hwpx(
1333
+ self, source: str, output: Optional[str] = None
1334
+ ) -> Dict[str, Any]:
1335
+ return self._services.preview_export.convert_hwp_to_hwpx(source, output)
1336
+
1337
+ def list_master_pages_histories_versions(self, path: str) -> Dict[str, Any]:
1338
+ return self._services.package_validation.list_master_pages_histories_versions(
1339
+ path
1340
+ )
1341
+
1342
+ def object_find_by_tag(
1343
+ self,
1344
+ path: str,
1345
+ tag_name: str,
1346
+ *,
1347
+ max_results: int = 200,
1348
+ ) -> Dict[str, Any]:
1349
+ return self._services.read_query.object_find_by_tag(
1350
+ path, tag_name, max_results=max_results
1351
+ )
1352
+
1353
+ def object_find_by_attr(
1354
+ self,
1355
+ path: str,
1356
+ element_type: str | None,
1357
+ attr: str,
1358
+ value: str | None,
1359
+ *,
1360
+ max_results: int = 200,
1361
+ ) -> Dict[str, Any]:
1362
+ return self._services.read_query.object_find_by_attr(
1363
+ path, element_type, attr, value, max_results=max_results
1364
+ )
1365
+
1366
+ def validate_structure(self, path: str, level: str = "basic") -> Dict[str, Any]:
1367
+ return self._services.package_validation.validate_structure(path, level)
1368
+
1369
+ def lint_text_conventions(
1370
+ self,
1371
+ path: str,
1372
+ *,
1373
+ max_line_len: Optional[int] = None,
1374
+ forbid_patterns: Optional[Sequence[str]] = None,
1375
+ ) -> Dict[str, Any]:
1376
+ return self._services.package_validation.lint_text_conventions(
1377
+ path, max_line_len=max_line_len, forbid_patterns=forbid_patterns
1378
+ )
1379
+
1380
+ def plan_edit(
1381
+ self,
1382
+ *,
1383
+ path: str,
1384
+ operations: Sequence[Dict[str, Any]],
1385
+ trace_id: Optional[str] = None,
1386
+ ) -> Dict[str, Any]:
1387
+ return self._services.planning.plan_edit(
1388
+ path=path, operations=operations, trace_id=trace_id
1389
+ )
1390
+
1391
+ def preview_edit(
1392
+ self,
1393
+ *,
1394
+ plan_id: str,
1395
+ trace_id: Optional[str] = None,
1396
+ ) -> Dict[str, Any]:
1397
+ return self._services.planning.preview_edit(plan_id=plan_id, trace_id=trace_id)
1398
+
1399
+ def apply_edit(
1400
+ self,
1401
+ *,
1402
+ plan_id: str,
1403
+ confirm: bool,
1404
+ idempotency_key: Optional[str] = None,
1405
+ trace_id: Optional[str] = None,
1406
+ ) -> Dict[str, Any]:
1407
+ return self._services.planning.apply_edit(
1408
+ plan_id=plan_id,
1409
+ confirm=confirm,
1410
+ idempotency_key=idempotency_key,
1411
+ trace_id=trace_id,
1412
+ )
1413
+
1414
+ def search(
1415
+ self,
1416
+ *,
1417
+ path: str,
1418
+ pattern: str,
1419
+ scope: Optional[str] = None,
1420
+ is_regex: bool = False,
1421
+ limit: int = 20,
1422
+ ) -> Dict[str, Any]:
1423
+ return self._services.planning.search(
1424
+ path=path, pattern=pattern, scope=scope, is_regex=is_regex, limit=limit
1425
+ )
1426
+
1427
+ def get_context(
1428
+ self,
1429
+ *,
1430
+ path: str,
1431
+ target: Dict[str, Any],
1432
+ window: int = 1,
1433
+ ) -> Dict[str, Any]:
1434
+ return self._services.planning.get_context(
1435
+ path=path, target=target, window=window
1436
+ )
1437
+
1438
+ def package_get_xml(self, path: str, part_name: str) -> Dict[str, Any]:
1439
+ return self._services.package_validation.package_get_xml(path, part_name)