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,55 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Durable server-enforced HWPX workflow engine contracts."""
3
+
4
+ from .models import (
5
+ ActionResult,
6
+ WORKFLOW_SCHEMA_VERSION,
7
+ WorkFamily,
8
+ WorkOrder,
9
+ WorkflowBudget,
10
+ WorkflowEvent,
11
+ WorkflowPolicy,
12
+ WorkflowRecord,
13
+ WorkflowState,
14
+ )
15
+ from .dispatcher import AllowlistedDispatcher, DispatchOutcome, FAMILY_TOOL_ALLOWLISTS
16
+ from .policy import ActionRequest, PolicyViolation, WorkflowPolicyEngine
17
+ from .rendering import (
18
+ RENDER_SCHEMA_VERSION,
19
+ NullRenderBackend,
20
+ RenderBackend,
21
+ RenderJob,
22
+ RenderReceipt,
23
+ RenderStatus,
24
+ )
25
+ from .state_machine import ALLOWED_TRANSITIONS, InvalidTransition
26
+ from .store import WorkflowConflict, WorkflowNotFound, WorkflowStore
27
+
28
+ __all__ = [
29
+ "ActionResult",
30
+ "ALLOWED_TRANSITIONS",
31
+ "ActionRequest",
32
+ "AllowlistedDispatcher",
33
+ "DispatchOutcome",
34
+ "FAMILY_TOOL_ALLOWLISTS",
35
+ "InvalidTransition",
36
+ "PolicyViolation",
37
+ "RENDER_SCHEMA_VERSION",
38
+ "NullRenderBackend",
39
+ "RenderBackend",
40
+ "RenderJob",
41
+ "RenderReceipt",
42
+ "RenderStatus",
43
+ "WORKFLOW_SCHEMA_VERSION",
44
+ "WorkFamily",
45
+ "WorkOrder",
46
+ "WorkflowBudget",
47
+ "WorkflowConflict",
48
+ "WorkflowEvent",
49
+ "WorkflowNotFound",
50
+ "WorkflowPolicy",
51
+ "WorkflowPolicyEngine",
52
+ "WorkflowRecord",
53
+ "WorkflowState",
54
+ "WorkflowStore",
55
+ ]
@@ -0,0 +1,482 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Typed family adapters for the high-level workflow service."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Mapping
10
+
11
+ from hwpx_automation.document_state import document_revision
12
+
13
+ from .models import WorkFamily, WorkflowRecord
14
+ from .policy import ActionRequest, PolicyViolation
15
+
16
+
17
+ class AdapterAbstention(RuntimeError):
18
+ """The typed request is unsupported or incomplete and needs human review."""
19
+
20
+ def __init__(self, code: str, message: str) -> None:
21
+ self.code = code
22
+ super().__init__(message)
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class WorkflowAdapter:
27
+ family: WorkFamily
28
+
29
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
30
+ raise NotImplementedError
31
+
32
+ def execution_action(self, record: WorkflowRecord) -> ActionRequest | None:
33
+ raise NotImplementedError
34
+
35
+ def prepare_execution(self, record: WorkflowRecord) -> None:
36
+ return None
37
+
38
+ def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
39
+ """Return the mandatory, read-only quality gates for this family."""
40
+
41
+ return ()
42
+
43
+ def execution_evidence_ok(self, record: WorkflowRecord, result: Any) -> bool:
44
+ return isinstance(result, Mapping) and result.get("ok") is not False
45
+
46
+ def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
47
+ """Fail closed when a mandatory verifier is absent or explicitly fails."""
48
+
49
+ actions = self.verification_actions(record)
50
+ if len(results) != len(actions):
51
+ return False
52
+ for result in results:
53
+ if not isinstance(result, Mapping):
54
+ return False
55
+ if result.get("ok") is False or result.get("pass") is False:
56
+ return False
57
+ return True
58
+
59
+ def recon_ok(self, result: Any) -> bool:
60
+ return not isinstance(result, Mapping) or result.get("ok", True) is not False
61
+
62
+
63
+ class ReadExtractAdapter(WorkflowAdapter):
64
+ _TOOLS = {
65
+ "text": "get_document_text",
66
+ "info": "get_document_info",
67
+ "outline": "get_document_outline",
68
+ "map": "get_document_map",
69
+ "markdown": "hwpx_to_markdown",
70
+ "json": "hwpx_extract_json",
71
+ }
72
+
73
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
74
+ if not record.work_order.source_path:
75
+ raise AdapterAbstention("SOURCE_REQUIRED", "read/extract requires a source document")
76
+ operation = str(record.work_order.parameters.get("operation", "info"))
77
+ tool = self._TOOLS.get(operation)
78
+ if tool is None:
79
+ raise AdapterAbstention("UNSUPPORTED_READ_OPERATION", f"unsupported read operation: {operation}")
80
+ arguments = dict(record.work_order.parameters.get("arguments") or {})
81
+ arguments.setdefault("filename", record.work_order.source_path)
82
+ return ActionRequest(tool_name=tool, arguments=arguments)
83
+
84
+ def execution_action(self, record: WorkflowRecord) -> ActionRequest | None:
85
+ return None
86
+
87
+
88
+ class TransactionalEditAdapter(WorkflowAdapter):
89
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
90
+ return ActionRequest(tool_name="get_document_info", arguments={"filename": record.work_order.source_path})
91
+
92
+ def prepare_execution(self, record: WorkflowRecord) -> None:
93
+ source = Path(record.work_order.source_path or "")
94
+ output = Path(record.work_order.output_path or "")
95
+ if output.exists():
96
+ if document_revision(output) != record.work_order.expected_revision:
97
+ raise PolicyViolation("OUTPUT_ALREADY_EXISTS", "workflow output exists with unrelated content")
98
+ return
99
+ output.parent.mkdir(parents=True, exist_ok=True)
100
+ shutil.copy2(source, output)
101
+
102
+ def execution_action(self, record: WorkflowRecord) -> ActionRequest:
103
+ operations = record.work_order.parameters.get("operations")
104
+ if not isinstance(operations, list):
105
+ raise AdapterAbstention("OPERATIONS_REQUIRED", "transactional edit requires an operations array")
106
+ output = record.work_order.output_path
107
+ return ActionRequest(
108
+ tool_name="apply_edits",
109
+ arguments={
110
+ "filename": output,
111
+ "operations": operations,
112
+ "dry_run": False,
113
+ "expected_revision": record.work_order.expected_revision,
114
+ "idempotency_key": f"{record.work_order.idempotency_key}:apply",
115
+ },
116
+ destructive=True,
117
+ )
118
+
119
+ def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
120
+ return (
121
+ ActionRequest(
122
+ tool_name="doc_diff",
123
+ arguments={
124
+ "old_filename": record.work_order.source_path,
125
+ "new_filename": record.work_order.output_path,
126
+ },
127
+ ),
128
+ )
129
+
130
+ def execution_evidence_ok(self, record: WorkflowRecord, result: Any) -> bool:
131
+ return (
132
+ super().execution_evidence_ok(record, result)
133
+ and isinstance(result.get("semanticDiff"), Mapping)
134
+ )
135
+
136
+
137
+ class KnownTemplateFillAdapter(WorkflowAdapter):
138
+ @staticmethod
139
+ def _coordinate_operations(record: WorkflowRecord) -> list[dict[str, Any]] | None:
140
+ if record.work_order.parameters.get("mode") != "coordinate_table":
141
+ return None
142
+ raw = record.work_order.parameters.get("operations")
143
+ if not isinstance(raw, list) or not raw:
144
+ raise AdapterAbstention(
145
+ "FROZEN_FILL_OPERATIONS_REQUIRED",
146
+ "coordinate-table fill requires frozen fill_cell operations",
147
+ )
148
+ operations: list[dict[str, Any]] = []
149
+ coordinates: set[tuple[int, int, int]] = set()
150
+ for value in raw:
151
+ if not isinstance(value, Mapping) or set(value) != {
152
+ "op",
153
+ "table_index",
154
+ "row",
155
+ "col",
156
+ "text",
157
+ }:
158
+ raise AdapterAbstention(
159
+ "FROZEN_FILL_OPERATIONS_REQUIRED",
160
+ "coordinate-table fill accepts exact fill_cell operations only",
161
+ )
162
+ operation = dict(value)
163
+ indices = tuple(operation[key] for key in ("table_index", "row", "col"))
164
+ if (
165
+ operation["op"] != "fill_cell"
166
+ or any(isinstance(item, bool) or not isinstance(item, int) or item < 0 for item in indices)
167
+ or not isinstance(operation["text"], str)
168
+ or not operation["text"]
169
+ or len(operation["text"]) > 4_096
170
+ or indices in coordinates
171
+ ):
172
+ raise AdapterAbstention(
173
+ "FROZEN_FILL_OPERATIONS_REQUIRED",
174
+ "coordinate-table fill operation is invalid",
175
+ )
176
+ coordinates.add(indices)
177
+ operations.append(operation)
178
+ return operations
179
+
180
+ def _required(self, record: WorkflowRecord) -> tuple[Any, dict[str, Any]]:
181
+ baseline = record.work_order.parameters.get("baseline")
182
+ content = record.work_order.parameters.get("content")
183
+ if baseline is None or not isinstance(content, dict):
184
+ raise AdapterAbstention("TEMPLATE_INPUT_REQUIRED", "baseline and typed content are required")
185
+ return baseline, content
186
+
187
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
188
+ if self._coordinate_operations(record) is not None:
189
+ return ActionRequest(
190
+ tool_name="scan_form_guidance",
191
+ arguments={"filename": record.work_order.source_path},
192
+ )
193
+ baseline, content = self._required(record)
194
+ return ActionRequest(
195
+ tool_name="analyze_template_formfit",
196
+ arguments={
197
+ "source_filename": record.work_order.source_path,
198
+ "baseline": baseline,
199
+ "content": content,
200
+ "destination_filename": record.work_order.output_path,
201
+ },
202
+ )
203
+
204
+ def execution_action(self, record: WorkflowRecord) -> ActionRequest:
205
+ coordinate_operations = self._coordinate_operations(record)
206
+ if coordinate_operations is not None:
207
+ return ActionRequest(
208
+ tool_name="apply_table_ops",
209
+ arguments={
210
+ "filename": record.work_order.source_path,
211
+ "ops": coordinate_operations,
212
+ "output": record.work_order.output_path,
213
+ "dry_run": False,
214
+ "render_check": "off",
215
+ },
216
+ destructive=True,
217
+ )
218
+ baseline, content = self._required(record)
219
+ return ActionRequest(
220
+ tool_name="apply_template_formfit",
221
+ arguments={
222
+ "source_filename": record.work_order.source_path,
223
+ "baseline": baseline,
224
+ "content": content,
225
+ "destination_filename": record.work_order.output_path,
226
+ "confirm": True,
227
+ },
228
+ destructive=True,
229
+ )
230
+
231
+ def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
232
+ return (
233
+ ActionRequest(
234
+ tool_name="inspect_fill_residue",
235
+ arguments={
236
+ "filename": record.work_order.output_path,
237
+ "blank_path": record.work_order.source_path,
238
+ },
239
+ ),
240
+ ActionRequest(
241
+ tool_name="verify_form_fill",
242
+ arguments={
243
+ "filename": record.work_order.output_path,
244
+ "before_path": record.work_order.source_path,
245
+ "require": False,
246
+ },
247
+ ),
248
+ )
249
+
250
+ def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
251
+ return len(results) == 2 and all(
252
+ isinstance(result, Mapping) and result.get("ok") is True for result in results
253
+ )
254
+
255
+
256
+ class UnknownFormFillAdapter(WorkflowAdapter):
257
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
258
+ return ActionRequest(tool_name="scan_form_guidance", arguments={"filename": record.work_order.source_path})
259
+
260
+ def execution_action(self, record: WorkflowRecord) -> ActionRequest:
261
+ operation_kind = str(record.work_order.parameters.get("operationKind", "table"))
262
+ tools = {"table": "apply_table_ops", "body": "apply_body_ops"}
263
+ tool = tools.get(operation_kind)
264
+ operations = record.work_order.parameters.get("operations")
265
+ if tool is None or not isinstance(operations, list):
266
+ raise AdapterAbstention(
267
+ "FORM_OPERATIONS_REQUIRED",
268
+ "unknown-form fill requires operationKind=table|body and an operations array",
269
+ )
270
+ arguments = {
271
+ "filename": record.work_order.source_path,
272
+ "ops": operations,
273
+ "output": record.work_order.output_path,
274
+ "dry_run": False,
275
+ }
276
+ if tool == "apply_table_ops":
277
+ arguments["render_check"] = "off"
278
+ return ActionRequest(tool_name=tool, arguments=arguments, destructive=True)
279
+
280
+ def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
281
+ return (
282
+ ActionRequest(
283
+ tool_name="inspect_fill_residue",
284
+ arguments={
285
+ "filename": record.work_order.output_path,
286
+ "blank_path": record.work_order.source_path,
287
+ },
288
+ ),
289
+ ActionRequest(
290
+ tool_name="verify_form_fill",
291
+ arguments={
292
+ "filename": record.work_order.output_path,
293
+ "before_path": record.work_order.source_path,
294
+ "require": False,
295
+ },
296
+ ),
297
+ )
298
+
299
+ def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
300
+ return len(results) == 2 and all(
301
+ isinstance(result, Mapping) and result.get("ok") is True for result in results
302
+ )
303
+
304
+
305
+ class StructuralTableEditAdapter(WorkflowAdapter):
306
+ """Apply one frozen row clone plus coordinate fills through ToolSpec."""
307
+
308
+ @staticmethod
309
+ def _operations(record: WorkflowRecord) -> list[dict[str, Any]]:
310
+ raw = record.work_order.parameters.get("operations")
311
+ if not isinstance(raw, list) or len(raw) < 2:
312
+ raise AdapterAbstention(
313
+ "STRUCTURAL_OPERATIONS_REQUIRED",
314
+ "structural table edit requires a row clone and frozen fills",
315
+ )
316
+ operations = [dict(value) for value in raw if isinstance(value, Mapping)]
317
+ if len(operations) != len(raw):
318
+ raise AdapterAbstention(
319
+ "STRUCTURAL_OPERATIONS_REQUIRED", "structural operations are invalid"
320
+ )
321
+ clone = operations[0]
322
+ if (
323
+ set(clone) != {"op", "table_index", "ref_row", "count"}
324
+ or clone.get("op") != "insert_row_by_clone"
325
+ or clone.get("count") != 1
326
+ or any(
327
+ isinstance(clone.get(key), bool)
328
+ or not isinstance(clone.get(key), int)
329
+ or clone.get(key) < 0
330
+ for key in ("table_index", "ref_row")
331
+ )
332
+ ):
333
+ raise AdapterAbstention(
334
+ "STRUCTURAL_OPERATIONS_REQUIRED", "row clone operation is invalid"
335
+ )
336
+ target_row = clone["ref_row"] + 1
337
+ columns: set[int] = set()
338
+ for fill in operations[1:]:
339
+ if (
340
+ set(fill) != {"op", "table_index", "row", "col", "text"}
341
+ or fill.get("op") != "fill_cell"
342
+ or fill.get("table_index") != clone["table_index"]
343
+ or fill.get("row") != target_row
344
+ or isinstance(fill.get("col"), bool)
345
+ or not isinstance(fill.get("col"), int)
346
+ or fill["col"] < 0
347
+ or fill["col"] in columns
348
+ or not isinstance(fill.get("text"), str)
349
+ or not fill["text"]
350
+ or len(fill["text"]) > 4_096
351
+ ):
352
+ raise AdapterAbstention(
353
+ "STRUCTURAL_OPERATIONS_REQUIRED",
354
+ "frozen structural fill operation is invalid",
355
+ )
356
+ columns.add(fill["col"])
357
+ return operations
358
+
359
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
360
+ self._operations(record)
361
+ return ActionRequest(
362
+ tool_name="scan_form_guidance",
363
+ arguments={"filename": record.work_order.source_path},
364
+ )
365
+
366
+ def execution_action(self, record: WorkflowRecord) -> ActionRequest:
367
+ return ActionRequest(
368
+ tool_name="apply_table_ops",
369
+ arguments={
370
+ "filename": record.work_order.source_path,
371
+ "ops": self._operations(record),
372
+ "output": record.work_order.output_path,
373
+ "dry_run": False,
374
+ "render_check": "off",
375
+ },
376
+ destructive=True,
377
+ )
378
+
379
+ def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
380
+ return (
381
+ ActionRequest(
382
+ tool_name="doc_diff",
383
+ arguments={
384
+ "old_filename": record.work_order.source_path,
385
+ "new_filename": record.work_order.output_path,
386
+ },
387
+ ),
388
+ )
389
+
390
+
391
+ class TypedAuthoringAdapter(WorkflowAdapter):
392
+ def _plan(self, record: WorkflowRecord) -> dict[str, Any]:
393
+ plan = record.work_order.parameters.get("documentPlan")
394
+ if not isinstance(plan, dict):
395
+ raise AdapterAbstention("DOCUMENT_PLAN_REQUIRED", "typed authoring requires documentPlan")
396
+ return plan
397
+
398
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
399
+ return ActionRequest(tool_name="validate_document_plan", arguments={"document_plan": self._plan(record)})
400
+
401
+ def recon_ok(self, result: Any) -> bool:
402
+ return isinstance(result, Mapping) and bool(result.get("ok", result.get("can_create", False)))
403
+
404
+ def prepare_execution(self, record: WorkflowRecord) -> None:
405
+ output = Path(record.work_order.output_path or "")
406
+ if output.exists():
407
+ raise PolicyViolation("OUTPUT_ALREADY_EXISTS", "typed authoring will not overwrite an existing output")
408
+ output.parent.mkdir(parents=True, exist_ok=True)
409
+
410
+ def execution_action(self, record: WorkflowRecord) -> ActionRequest:
411
+ return ActionRequest(
412
+ tool_name="create_document_from_plan",
413
+ arguments={
414
+ "filename": record.work_order.output_path,
415
+ "document_plan": self._plan(record),
416
+ "verify_render": False,
417
+ },
418
+ destructive=True,
419
+ )
420
+
421
+ def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
422
+ actions = [
423
+ ActionRequest(
424
+ tool_name="inspect_document_authoring_quality",
425
+ arguments={
426
+ "filename": record.work_order.output_path,
427
+ "document_plan": self._plan(record),
428
+ "quality_profile": record.work_order.parameters.get("qualityProfile"),
429
+ },
430
+ )
431
+ ]
432
+ metadata = self._plan(record).get("metadata")
433
+ document_type = metadata.get("document_type") if isinstance(metadata, Mapping) else None
434
+ if document_type in {"gongmun", "official", "official_document", "공문"}:
435
+ actions.append(
436
+ ActionRequest(
437
+ tool_name="inspect_official_document_style",
438
+ arguments={"filename": record.work_order.output_path},
439
+ )
440
+ )
441
+ return tuple(actions)
442
+
443
+ def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
444
+ actions = self.verification_actions(record)
445
+ if len(results) != len(actions) or not results:
446
+ return False
447
+ quality = results[0]
448
+ if not isinstance(quality, Mapping) or quality.get("pass") is not True:
449
+ return False
450
+ return all(
451
+ isinstance(result, Mapping) and result.get("ok") is True
452
+ for result in results[1:]
453
+ )
454
+
455
+
456
+ class MustAbstainAdapter(WorkflowAdapter):
457
+ """A durable, no-tool family for an expected safety abstention."""
458
+
459
+ def recon_action(self, record: WorkflowRecord) -> ActionRequest:
460
+ raise AdapterAbstention(
461
+ "UNSUPPORTED_INTENT",
462
+ "must-abstain work orders never dispatch document tools",
463
+ )
464
+
465
+ def execution_action(self, record: WorkflowRecord) -> None:
466
+ return None
467
+
468
+
469
+ ADAPTERS: dict[WorkFamily, WorkflowAdapter] = {
470
+ WorkFamily.READ_EXTRACT: ReadExtractAdapter(WorkFamily.READ_EXTRACT),
471
+ WorkFamily.TRANSACTIONAL_EDIT: TransactionalEditAdapter(WorkFamily.TRANSACTIONAL_EDIT),
472
+ WorkFamily.KNOWN_TEMPLATE_FILL: KnownTemplateFillAdapter(WorkFamily.KNOWN_TEMPLATE_FILL),
473
+ WorkFamily.UNKNOWN_FORM_FILL: UnknownFormFillAdapter(WorkFamily.UNKNOWN_FORM_FILL),
474
+ WorkFamily.STRUCTURAL_TABLE_EDIT: StructuralTableEditAdapter(
475
+ WorkFamily.STRUCTURAL_TABLE_EDIT
476
+ ),
477
+ WorkFamily.TYPED_AUTHORING: TypedAuthoringAdapter(WorkFamily.TYPED_AUTHORING),
478
+ WorkFamily.MUST_ABSTAIN: MustAbstainAdapter(WorkFamily.MUST_ABSTAIN),
479
+ }
480
+
481
+
482
+ __all__ = ["ADAPTERS", "AdapterAbstention", "WorkflowAdapter"]