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,557 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable, Sequence
7
+ from typing import Any
8
+
9
+ from hwpx.quality.rendering import RenderBackend
10
+
11
+ from ..office.form_fill import (
12
+ inspect_fill_residue as inspect_form_fill_residue,
13
+ )
14
+ from ..office.form_fill import (
15
+ scan_form_guidance as scan_canonical_form_guidance,
16
+ )
17
+ from ..office.form_fill import (
18
+ score_form_fill as score_canonical_form_fill,
19
+ )
20
+ from ..office.rendering import resolve_hancom_backend
21
+ from ..storage import LocalDocumentStorage
22
+ from ..workspace import (
23
+ WorkspaceMissingParentGuard,
24
+ WorkspaceOutputGuard,
25
+ )
26
+ from .context import DocumentContext
27
+ from .save_policy import SavePolicy
28
+ from .transactions import TransactionService
29
+
30
+ logger = logging.getLogger("hwpx_automation.hwpx_ops")
31
+
32
+
33
+ class FormFieldService:
34
+ def __init__(
35
+ self,
36
+ context: DocumentContext,
37
+ save: SavePolicy,
38
+ transactions: TransactionService,
39
+ *,
40
+ render_backend_factory: Callable[[], RenderBackend] | None = None,
41
+ ) -> None:
42
+ self._context = context
43
+ self._save = save
44
+ self._transactions = transactions
45
+ self._render_backend_factory = render_backend_factory
46
+
47
+ def _resolve_render_backend(self) -> RenderBackend:
48
+ """Return the application-owned renderer for every core fill verdict.
49
+
50
+ Core intentionally does not discover Hancom. Keeping the factory at
51
+ this service boundary makes all three form-fill paths use the same
52
+ fail-closed injection seam, while tests can supply an observable
53
+ backend without replacing core's ``verify_fill`` judgement.
54
+ """
55
+
56
+ factory = self._render_backend_factory or resolve_hancom_backend
57
+ return factory()
58
+
59
+ def list_form_fields(
60
+ self,
61
+ path: str,
62
+ ) -> dict[str, Any]:
63
+ document, _resolved = self._context._open_document(path)
64
+ fields = document.list_form_fields()
65
+ return {
66
+ "fieldCount": len(fields),
67
+ "fields": fields,
68
+ "fallback": "table-label" if not fields else None,
69
+ }
70
+
71
+ def fill_form_field(
72
+ self,
73
+ path: str,
74
+ *,
75
+ value: str,
76
+ field_index: int | None = None,
77
+ field_id: str | None = None,
78
+ name: str | None = None,
79
+ dry_run: bool = False,
80
+ ) -> dict[str, Any]:
81
+ document, resolved = self._context._open_document(path)
82
+ result = document.fill_form_field(
83
+ value,
84
+ field_index=field_index,
85
+ field_id=field_id,
86
+ name=name,
87
+ )
88
+ result.update({"ok": True, "filename": path})
89
+ return self._transactions._with_transaction_verification(
90
+ result, document, resolved, dry_run=dry_run
91
+ )
92
+
93
+ def apply_table_ops(
94
+ self,
95
+ path: str,
96
+ ops: Sequence[dict[str, Any]],
97
+ *,
98
+ output: str | None = None,
99
+ render_check: str = "off",
100
+ dry_run: bool = False,
101
+ output_guard: (
102
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
103
+ ) = None,
104
+ publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
105
+ ) -> dict[str, Any]:
106
+ """Byte-preserving structural form-fill: apply cell fills + table structure
107
+ ops (delete_column/row/table, insert_row_by_clone, insert_block_by_clone)
108
+ preserving every untouched byte. Cells/tables may be addressed by
109
+ tableAnchor/cellAnchor (unique-or-skip) as well as index. Optional
110
+ real-Hancom render gate.
111
+
112
+ dryRun=true: identical pipeline (resolution/validation/fail-closed all
113
+ real) but writes NOTHING — returns transcript (per-op resolution + before/
114
+ after dims) and applied old→new texts as approval evidence for the user
115
+ consult loop. renderCheck still works on the would-be bytes."""
116
+ try:
117
+ from hwpx.table_patch import apply_table_ops as _apply
118
+ except Exception as exc: # pragma: no cover - dependency compatibility
119
+ raise self._context._new_error(
120
+ "TABLE_OPS_UNAVAILABLE",
121
+ "installed python-hwpx does not provide hwpx.table_patch.apply_table_ops",
122
+ ) from exc
123
+
124
+ source_path = self._context._resolve_path(path)
125
+ output_precondition = None
126
+ if isinstance(self._context.storage, LocalDocumentStorage):
127
+ output_precondition = (
128
+ output_guard
129
+ or self._context.storage.capture_output_precondition(
130
+ output if output else source_path
131
+ )
132
+ )
133
+ target_path = output_precondition.path
134
+ else:
135
+ target_path = (
136
+ self._context._resolve_path(output, must_exist=False)
137
+ if output and dry_run
138
+ else self._context._resolve_output_path(output)
139
+ if output
140
+ else source_path
141
+ )
142
+ result = _apply(source_path, list(ops), dry_run=dry_run)
143
+ payload = result.to_dict()
144
+ if dry_run:
145
+ payload["dryRun"] = True
146
+ payload["outputPath"] = None
147
+ else:
148
+ payload["outputPath"] = str(target_path)
149
+
150
+ if render_check and render_check != "off":
151
+ try:
152
+ from hwpx.table_patch import verify_fill
153
+
154
+ report = verify_fill(
155
+ source_path,
156
+ result.data,
157
+ oracle=self._resolve_render_backend(),
158
+ require=(render_check == "required"),
159
+ )
160
+ payload["renderVerdict"] = {
161
+ "renderChecked": report.render_checked,
162
+ "ok": report.ok,
163
+ "overflowDetected": report.overflow_detected,
164
+ "overlapDetected": report.overlap_detected,
165
+ "pageCountChanged": report.page_count_changed,
166
+ "warnings": list(report.warnings),
167
+ "errors": list(report.errors),
168
+ }
169
+ if report.render_checked and not report.ok:
170
+ payload["ok"] = False
171
+ if render_check == "required":
172
+ raise self._context._new_error(
173
+ "RENDER_CHECK_REQUIRED_FAILED",
174
+ "required render detected overflow, overlap, or layout regression",
175
+ )
176
+ except Exception as exc:
177
+ if render_check == "required":
178
+ raise self._context._new_error(
179
+ "RENDER_CHECK_REQUIRED_FAILED", str(exc)
180
+ ) from exc
181
+ payload["renderVerdict"] = {"renderChecked": False, "note": str(exc)}
182
+ # A required render is a pre-publication gate: only replace the
183
+ # destination after the candidate has passed it.
184
+ if (
185
+ payload.get("ok") is not False
186
+ and not dry_run
187
+ and (
188
+ not result.byte_identical
189
+ or target_path.resolve(strict=False)
190
+ != source_path.resolve(strict=False)
191
+ )
192
+ ):
193
+ payload = self._save._write_patched(
194
+ target_path,
195
+ result.data,
196
+ payload,
197
+ output_guard=output_guard,
198
+ output_precondition=output_precondition,
199
+ publication_sink=publication_sink,
200
+ )
201
+ return payload
202
+
203
+ def verify_form_fill(
204
+ self,
205
+ path: str,
206
+ before_path: str,
207
+ *,
208
+ require: bool = False,
209
+ ) -> dict[str, Any]:
210
+ """Render before/after in REAL Hancom and judge overflow/overlap/layout.
211
+ Honest degrade (renderChecked=false) with no oracle unless require=true."""
212
+ try:
213
+ from hwpx.table_patch import verify_fill
214
+ except Exception as exc: # pragma: no cover - dependency compatibility
215
+ raise self._context._new_error(
216
+ "VERIFY_UNAVAILABLE",
217
+ "installed python-hwpx does not provide hwpx.table_patch.verify_fill",
218
+ ) from exc
219
+ after = self._context._resolve_path(path)
220
+ before = self._context._resolve_path(before_path)
221
+ report = verify_fill(
222
+ before,
223
+ after,
224
+ oracle=self._resolve_render_backend(),
225
+ require=require,
226
+ )
227
+ return {
228
+ "renderChecked": report.render_checked,
229
+ "ok": report.ok,
230
+ "overflowDetected": report.overflow_detected,
231
+ "overlapDetected": report.overlap_detected,
232
+ "pageCountChanged": report.page_count_changed,
233
+ "warnings": list(report.warnings),
234
+ "errors": list(report.errors),
235
+ }
236
+
237
+ def score_form_fill(
238
+ self,
239
+ path: str,
240
+ gold_path: str,
241
+ blank_path: str,
242
+ *,
243
+ run_render: bool = True,
244
+ expected_pages: int | None = None,
245
+ ) -> dict[str, Any]:
246
+ """Score a filled evaluation-plan form against a gold reference on 5 axes
247
+ (A render cleanliness / B byte fidelity vs blank / C structure vs gold /
248
+ D content completeness / E compliance) -> weighted 0-100 + per-axis gap
249
+ report. The fitness function of the form-fill quality loop.
250
+
251
+ ``path`` = produced fill, ``goldPath`` = accepted reference of the same
252
+ form family, ``blankPath`` = the empty province form. A requires a real
253
+ Hancom render (renderCheck); with no oracle A is ``unverified`` (never a
254
+ silent pass). Set runRender=false for a fast structural-only pass."""
255
+ produced = self._context._resolve_path(path)
256
+ gold = self._context._resolve_path(gold_path)
257
+ blank = self._context._resolve_path(blank_path)
258
+ card = score_canonical_form_fill(
259
+ produced,
260
+ gold,
261
+ blank,
262
+ run_render=run_render,
263
+ expected_pages=expected_pages,
264
+ )
265
+ return card.to_dict()
266
+
267
+ def apply_body_ops(
268
+ self,
269
+ path: str,
270
+ ops: Sequence[dict[str, Any]],
271
+ *,
272
+ output: str | None = None,
273
+ dry_run: bool = False,
274
+ output_guard: (
275
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
276
+ ) = None,
277
+ publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
278
+ ) -> dict[str, Any]:
279
+ """Byte-preserving BODY(표 밖 직속 문단) ops — Stage 2 결정표의 본문 어휘.
280
+
281
+ ops: replace_text{find,replace,count=1: <hp:t> 텍스트 안에서만, 개수 불일치
282
+ refuse} · delete_paragraph{index: 표 품은 문단 refuse} ·
283
+ insert_paragraph_by_clone{ref_index,count,texts?: 참조 문단 서식 verbatim
284
+ 상속} · reorder_paragraphs{start,end,order}. index는 op 실행 시점 기준.
285
+ dryRun=true면 아무것도 쓰지 않고 transcript만(승인 근거)."""
286
+ try:
287
+ from hwpx.body_patch import apply_body_ops as _apply
288
+ except Exception as exc: # pragma: no cover - dependency compatibility
289
+ raise self._context._new_error(
290
+ "BODY_OPS_UNAVAILABLE",
291
+ "installed python-hwpx does not provide hwpx.body_patch.apply_body_ops",
292
+ ) from exc
293
+ source_path = self._context._resolve_path(path)
294
+ output_precondition = None
295
+ if isinstance(self._context.storage, LocalDocumentStorage):
296
+ output_precondition = (
297
+ output_guard
298
+ or self._context.storage.capture_output_precondition(
299
+ output if output else source_path
300
+ )
301
+ )
302
+ target_path = output_precondition.path
303
+ else:
304
+ target_path = (
305
+ self._context._resolve_path(output, must_exist=False)
306
+ if output and dry_run
307
+ else self._context._resolve_output_path(output)
308
+ if output
309
+ else source_path
310
+ )
311
+ result = _apply(source_path, list(ops), dry_run=dry_run)
312
+ payload = result.to_dict()
313
+ if dry_run:
314
+ payload["dryRun"] = True
315
+ payload["outputPath"] = None
316
+ else:
317
+ payload["outputPath"] = str(target_path)
318
+ if not result.byte_identical or target_path.resolve(
319
+ strict=False
320
+ ) != source_path.resolve(strict=False):
321
+ payload = self._save._write_patched(
322
+ target_path,
323
+ result.data,
324
+ payload,
325
+ output_guard=output_guard,
326
+ output_precondition=output_precondition,
327
+ publication_sink=publication_sink,
328
+ )
329
+ return payload
330
+
331
+ def inspect_fill_residue(
332
+ self,
333
+ path: str,
334
+ *,
335
+ blank_path: str | None = None,
336
+ ) -> dict[str, Any]:
337
+ """채움본 잔존물 zero-체크(비변형) — 제출 전 기계 게이트(Stage 3).
338
+
339
+ blank를 주면 범례 기반 신호까지: 삭제색 잔존·미수정 샘플(수정색이 blank와
340
+ 동일=prose 샘플 미교체) = ERROR. placeholder ◯◯◯/□□□=ERROR, **=각주와
341
+ 중의적이라 needs_review, 고아 마커=needs_review. ok=true는 필요조건일 뿐 —
342
+ 제출 확언은 렌더 PDF를 사람이 전 페이지 본 뒤에만."""
343
+ produced = self._context._resolve_path(path)
344
+ blank = self._context._resolve_path(blank_path) if blank_path else None
345
+ return inspect_form_fill_residue(produced, blank=blank).to_dict()
346
+
347
+ def scan_form_guidance(self, path: str, *, max_items: int = 60) -> dict[str, Any]:
348
+ """Recon an unfamiliar form (NON-MUTATING) — universal form-fill Stage 1.
349
+
350
+ Walks every run INCLUDING table-cell interiors and table captions, parses
351
+ the form's own colour legend (e.g. "검정 유지/파랑 수정/빨강 삭제"), and
352
+ reports candidates with table_patch-compatible addresses: delete candidates
353
+ (legend-delete colour + guidance keywords), modify targets, placeholder
354
+ tokens (◯◯◯/**/□□□), conditional-choice blocks, empty cells with neighbour
355
+ label + charPr format context, and an honest question list. Candidates are
356
+ proposals — destructive ops still require user approval."""
357
+ resolved = self._context._resolve_path(path)
358
+ report = scan_canonical_form_guidance(resolved)
359
+ limit = max(1, int(max_items))
360
+
361
+ def _cand(c) -> dict[str, Any]:
362
+ cell = None
363
+ if c.cell is not None:
364
+ cell = {
365
+ "tableIndex": c.cell.table_index,
366
+ "row": c.cell.row,
367
+ "col": c.cell.col,
368
+ }
369
+ return {
370
+ "location": c.location,
371
+ "signals": c.signals,
372
+ "confidence": c.confidence,
373
+ "textPreview": c.text_preview,
374
+ "cell": cell,
375
+ }
376
+
377
+ def _cap(items) -> list[dict[str, Any]]:
378
+ return [_cand(c) for c in items[:limit]]
379
+
380
+ return {
381
+ "legend": [
382
+ {
383
+ "colorWord": b.color_word,
384
+ "family": b.family,
385
+ "exactHex": b.exact_hex,
386
+ "action": b.action,
387
+ "sourceText": b.source_text,
388
+ }
389
+ for b in report.legend
390
+ ],
391
+ "colorInventory": report.color_inventory,
392
+ "deleteCandidates": _cap(report.delete_candidates),
393
+ "deleteCandidatesTotal": len(report.delete_candidates),
394
+ "modifyCandidatesByTable": report.modify_candidates_by_table,
395
+ "emptyCellCandidates": _cap(report.empty_cell_candidates),
396
+ "emptyCellTotal": len(report.empty_cell_candidates),
397
+ "placeholderCandidates": _cap(report.placeholder_candidates),
398
+ "conditionalChoices": _cap(report.conditional_choices),
399
+ "questions": report.questions,
400
+ "stats": report.stats,
401
+ "limitations": report.limitations,
402
+ "markdownReport": report.to_markdown(),
403
+ }
404
+
405
+ _EVALPLAN_PHASES = ("structural", "all", "clean")
406
+
407
+ def apply_evalplan_fill(
408
+ self,
409
+ path: str,
410
+ review_md: str,
411
+ *,
412
+ phase: str = "all",
413
+ output: str | None = None,
414
+ render_check: str = "off",
415
+ score_gold_path: str | None = None,
416
+ expected_pages: int | None = None,
417
+ output_guard: (
418
+ WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
419
+ ) = None,
420
+ publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
421
+ ) -> dict[str, Any]:
422
+ """Whole-form 평가계획 fill: {blank province form + review markdown} ->
423
+ byte-preserving gold-quality 채움본 in ONE call. Runs the structure-driven
424
+ recipe (delete red/optional tables + 정기시험 column, fill 운영계획/성취기준/
425
+ 성취수준/성취율/반영비율/rubrics incl. 채점기준 ladder/prose sections),
426
+ preserving the blank's formatting (never regenerates). Works on BOTH the
427
+ 2015-개정 and 2022-개정 form families (auto-detected from the blank + review).
428
+
429
+ ``path`` = blank form, ``reviewMd`` = the structured review markdown
430
+ (Ⅰ 운영계획 + [1]~[11]). ``phase`` selects how far the recipe runs:
431
+ ``"structural"`` = confident deletions only, ``"all"`` (default; the prior
432
+ behaviour) = structural + per-region content fills, ``"clean"`` = ``"all"``
433
+ plus core's deterministic post-fill cleanup (title/teacher/정의적 fills,
434
+ instruction-scaffolding + orphan prune, red-strip, blue->black recolor,
435
+ caption strip) that yields a submittable 채움본 with no bespoke driver; the
436
+ cleanup report is surfaced under ``contentReport.finalize``. Returns the
437
+ produced path + per-region contentReport with rubricNeedsReview (honest-defer
438
+ count, never silent). Set renderCheck='required' to gate on a real Hancom
439
+ render; pass scoreGoldPath (an accepted form of the same family) to also
440
+ return the 5-axis scorecard."""
441
+ if phase not in self._EVALPLAN_PHASES:
442
+ raise self._context._new_error(
443
+ "EVALPLAN_PHASE_INVALID",
444
+ f"phase must be one of {self._EVALPLAN_PHASES}, got {phase!r}",
445
+ )
446
+ try:
447
+ from hwpx_automation.office.evalplan import (
448
+ expected_skeleton,
449
+ fill_evalplan,
450
+ parse_review_file,
451
+ )
452
+ except Exception as exc: # pragma: no cover - broken package guard
453
+ raise self._context._new_error(
454
+ "EVALPLAN_FILL_UNAVAILABLE",
455
+ "installed python-hwpx-automation does not provide the evalplan runtime",
456
+ ) from exc
457
+
458
+ blank = self._context._resolve_path(path)
459
+ md = self._context._resolve_path(review_md)
460
+ output_precondition = None
461
+ if isinstance(self._context.storage, LocalDocumentStorage):
462
+ output_precondition = (
463
+ output_guard
464
+ or self._context.storage.capture_output_precondition(
465
+ output if output else blank
466
+ )
467
+ )
468
+ target_path = output_precondition.path
469
+ else:
470
+ target_path = (
471
+ self._context._resolve_output_path(output) if output else blank
472
+ )
473
+ content = parse_review_file(md)
474
+ res = fill_evalplan(blank, content, phase=phase)
475
+ data = res["_data"]
476
+
477
+ report = res.get("content_report", {})
478
+ rubric_nr = [
479
+ s
480
+ for s in report.get("rubrics", {}).get("skipped", [])
481
+ if "NEEDS_REVIEW" in s
482
+ ]
483
+ payload: dict[str, Any] = {
484
+ "ok": bool(res.get("ok")),
485
+ "outputPath": str(target_path),
486
+ "byteIdentical": bool(res.get("byteIdentical")),
487
+ "transcript": res.get("transcript", []),
488
+ "expectedSkeleton": res.get("expected_skeleton"),
489
+ "contentReport": report,
490
+ "rubricNeedsReview": len(rubric_nr),
491
+ "needsReviewNotes": rubric_nr,
492
+ "changedParts": res.get("changedParts", []),
493
+ "skipped": res.get("skipped", []),
494
+ }
495
+ if render_check and render_check != "off":
496
+ try:
497
+ from hwpx.table_patch import verify_fill
498
+
499
+ verdict = verify_fill(
500
+ blank,
501
+ data,
502
+ oracle=self._resolve_render_backend(),
503
+ require=(render_check == "required"),
504
+ )
505
+ payload["renderVerdict"] = {
506
+ "renderChecked": verdict.render_checked,
507
+ "ok": verdict.ok,
508
+ "overflowDetected": verdict.overflow_detected,
509
+ "overlapDetected": verdict.overlap_detected,
510
+ "pageCountChanged": verdict.page_count_changed,
511
+ "warnings": list(verdict.warnings),
512
+ "errors": list(verdict.errors),
513
+ }
514
+ if verdict.render_checked and not verdict.ok:
515
+ payload["ok"] = False
516
+ if render_check == "required":
517
+ raise self._context._new_error(
518
+ "RENDER_CHECK_REQUIRED_FAILED",
519
+ "required evalplan render detected overflow, overlap, or layout regression",
520
+ )
521
+ except Exception as exc:
522
+ if render_check == "required":
523
+ raise self._context._new_error(
524
+ "RENDER_CHECK_REQUIRED_FAILED", str(exc)
525
+ ) from exc
526
+ payload["renderVerdict"] = {"renderChecked": False, "note": str(exc)}
527
+
528
+ # Do not publish a partial/failed domain result. Required rendering is
529
+ # evaluated against candidate bytes before the atomic destination swap.
530
+ if payload["ok"] and (
531
+ not res.get("byteIdentical", True)
532
+ or target_path.resolve(strict=False) != blank.resolve(strict=False)
533
+ ):
534
+ payload = self._save._write_patched(
535
+ target_path,
536
+ data,
537
+ payload,
538
+ output_guard=output_guard,
539
+ output_precondition=output_precondition,
540
+ publication_sink=publication_sink,
541
+ )
542
+
543
+ if score_gold_path:
544
+ try:
545
+ card = score_canonical_form_fill(
546
+ target_path,
547
+ self._context._resolve_path(score_gold_path),
548
+ blank,
549
+ content=md,
550
+ expected_skeleton=expected_skeleton(content, blank),
551
+ run_render=bool(render_check and render_check != "off"),
552
+ expected_pages=expected_pages,
553
+ )
554
+ payload["scorecard"] = card.to_dict()
555
+ except Exception as exc: # pragma: no cover - scoring optional
556
+ payload["scorecard"] = {"error": str(exc)}
557
+ return payload