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,223 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Typed public outputs for canonical and compatibility form tools.
3
+
4
+ The server keeps returning ordinary dictionaries. These models are bound as
5
+ return annotations so FastMCP validates those dictionaries and publishes the
6
+ same closed output contract that callers actually receive. Legacy form-fill
7
+ responses intentionally keep an open payload because their long-standing
8
+ compatibility fields vary by route; their identifying compatibility envelope
9
+ remains typed so they cannot silently accept a malformed canonical response.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Literal
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field, RootModel
17
+
18
+ from .mixed_form import (
19
+ FormVerificationReceipt,
20
+ MixedFormCompiledPlanInput,
21
+ MixedFormResolution,
22
+ )
23
+
24
+
25
+ _REVISION_PATTERN = r"^sha256:[0-9a-f]{64}$"
26
+
27
+
28
+ class _StrictOutput(BaseModel):
29
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
30
+
31
+
32
+ class _LegacyOutput(BaseModel):
33
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
34
+
35
+
36
+ class _AnalysisSource(_StrictOutput):
37
+ filename: str
38
+ revision: str = Field(pattern=_REVISION_PATTERN)
39
+ unchanged_after_analysis: Literal[True] = Field(alias="unchangedAfterAnalysis")
40
+
41
+
42
+ class _AnalysisOutputRef(_StrictOutput):
43
+ filename: str
44
+ unchanged_after_analysis: Literal[True] = Field(alias="unchangedAfterAnalysis")
45
+
46
+
47
+ class CanonicalMixedFormAnalysisOutput(_StrictOutput):
48
+ schema_version: Literal["hwpx.mixed-form-analysis/v1"] = Field(
49
+ alias="schemaVersion"
50
+ )
51
+ mutated: Literal[False]
52
+ source: _AnalysisSource
53
+ output: _AnalysisOutputRef
54
+ compiled_plan: MixedFormCompiledPlanInput = Field(alias="compiledPlan")
55
+ plan_hash: str = Field(alias="planHash", pattern=_REVISION_PATTERN)
56
+ request_hash: str = Field(alias="requestHash", pattern=_REVISION_PATTERN)
57
+ resolutions: list[MixedFormResolution]
58
+ open_safety: dict[str, Any] = Field(alias="openSafety")
59
+ next_tool: Literal["apply_form_fill"] = Field(alias="nextTool")
60
+
61
+
62
+ class _LegacyFormFillCompatibility(_StrictOutput):
63
+ schema_version: Literal["hwpx.formfill.v1"] = Field(alias="schemaVersion")
64
+ status: Literal["retained"]
65
+ canonical_input: str = Field(alias="canonicalInput", min_length=1)
66
+
67
+
68
+ class LegacyFormFillAnalysisOutput(_LegacyOutput):
69
+ schema_version: Literal["hwpx.formfill.v1"] = Field(alias="schemaVersion")
70
+ plan_id: str = Field(min_length=1)
71
+ mutated: Literal[False]
72
+ compatibility: _LegacyFormFillCompatibility
73
+
74
+
75
+ class AnalyzeFormFillOutput(
76
+ RootModel[CanonicalMixedFormAnalysisOutput | LegacyFormFillAnalysisOutput]
77
+ ):
78
+ """Canonical analysis or the retained ``hwpx.formfill.v1`` response."""
79
+
80
+
81
+ class _AgentOutputRef(_StrictOutput):
82
+ filename: str
83
+
84
+
85
+ class _PublicCommandResult(_StrictOutput):
86
+ command_id: str = Field(alias="commandId", min_length=1)
87
+ op: str = Field(min_length=1)
88
+ ok: bool
89
+ path: str | None = None
90
+ parent_path: str | None = Field(default=None, alias="parentPath")
91
+ warning_count: int = Field(alias="warningCount", ge=0)
92
+ changed_property_names: list[str] = Field(alias="changedPropertyNames")
93
+ generated_identity_count: int = Field(alias="generatedIdentityCount", ge=0)
94
+
95
+
96
+ class CanonicalMixedFormApplyOutput(_StrictOutput):
97
+ schema_version: Literal["hwpx.agent-batch-result/v1"] = Field(
98
+ alias="schemaVersion"
99
+ )
100
+ ok: bool
101
+ rolled_back: bool = Field(alias="rolledBack")
102
+ dry_run: bool = Field(alias="dryRun")
103
+ input_revision: str = Field(alias="inputRevision", pattern=_REVISION_PATTERN)
104
+ document_revision: str | None = Field(
105
+ alias="documentRevision",
106
+ pattern=_REVISION_PATTERN,
107
+ )
108
+ output: _AgentOutputRef
109
+ command_results: list[_PublicCommandResult] = Field(alias="commandResults")
110
+ semantic_diff: dict[str, Any] = Field(alias="semanticDiff")
111
+ verification_report: dict[str, Any] = Field(alias="verificationReport")
112
+ error: dict[str, Any] | None = None
113
+ plan_hash: str = Field(alias="planHash", pattern=_REVISION_PATTERN)
114
+ request_hash: str = Field(alias="requestHash", pattern=_REVISION_PATTERN)
115
+ resolutions: list[MixedFormResolution]
116
+ verification_receipt: FormVerificationReceipt = Field(
117
+ alias="verificationReceipt"
118
+ )
119
+ open_safety: dict[str, Any] = Field(alias="openSafety")
120
+
121
+
122
+ class LegacyFormFillApplyOutput(_LegacyOutput):
123
+ handoff_status: Literal["ready", "blocked"]
124
+ compatibility: _LegacyFormFillCompatibility
125
+
126
+
127
+ class ApplyFormFillOutput(
128
+ RootModel[CanonicalMixedFormApplyOutput | LegacyFormFillApplyOutput]
129
+ ):
130
+ """Canonical atomic apply or the retained ``hwpx.formfill.v1`` response."""
131
+
132
+
133
+ class _LegacyRenderCompatibility(_StrictOutput):
134
+ status: Literal["retained-render-verifier"]
135
+ canonical_input: Literal["plan: hwpx.mixed-form-compiled-plan/v1"] = Field(
136
+ alias="canonicalInput"
137
+ )
138
+
139
+
140
+ class LegacyFormFillVerifyOutput(_LegacyOutput):
141
+ render_checked: bool = Field(alias="renderChecked")
142
+ ok: bool
143
+ compatibility: _LegacyRenderCompatibility
144
+
145
+
146
+ class VerifyFormFillOutput(
147
+ RootModel[FormVerificationReceipt | LegacyFormFillVerifyOutput]
148
+ ):
149
+ """Canonical receipt or the retained two-file render verifier response."""
150
+
151
+
152
+ class _SpecializedCompatibility(_StrictOutput):
153
+ status: Literal[
154
+ "specialized-semantics-preserved",
155
+ "specialized-verification-failed",
156
+ "rolled-back",
157
+ ]
158
+ canonical_receipt: Literal["hwpx.form-verification-receipt/v1"] = Field(
159
+ alias="canonicalReceipt"
160
+ )
161
+
162
+
163
+ class _SpecializedFormOutput(_StrictOutput):
164
+ ok: bool
165
+ dry_run: bool = Field(alias="dryRun")
166
+ output_path: str | None = Field(alias="outputPath")
167
+ changed_parts: list[str] = Field(alias="changedParts")
168
+ byte_identical: bool = Field(alias="byteIdentical")
169
+ skipped: list[Any]
170
+ rolled_back: bool = Field(alias="rolledBack")
171
+ verification_receipt: FormVerificationReceipt = Field(
172
+ alias="verificationReceipt"
173
+ )
174
+ compatibility: _SpecializedCompatibility
175
+ open_safety: dict[str, Any] | None = Field(default=None, alias="openSafety")
176
+ verification_report: dict[str, Any] | None = Field(
177
+ default=None,
178
+ alias="verificationReport",
179
+ )
180
+ render_verdict: dict[str, Any] | None = Field(
181
+ default=None,
182
+ alias="renderVerdict",
183
+ )
184
+ document_revision: str | None = Field(
185
+ default=None,
186
+ alias="documentRevision",
187
+ pattern=_REVISION_PATTERN,
188
+ )
189
+ error: dict[str, Any] | None = None
190
+
191
+
192
+ class ApplyTableOpsOutput(_SpecializedFormOutput):
193
+ applied: list[dict[str, Any]]
194
+ zip_method: str = Field(alias="zipMethod")
195
+ transcript: list[Any] | None = None
196
+
197
+
198
+ class ApplyBodyOpsOutput(_SpecializedFormOutput):
199
+ transcript: list[Any]
200
+
201
+
202
+ class ApplyEvalplanFillOutput(_SpecializedFormOutput):
203
+ transcript: list[Any]
204
+ expected_skeleton: Any = Field(alias="expectedSkeleton")
205
+ content_report: dict[str, Any] = Field(alias="contentReport")
206
+ rubric_needs_review: int = Field(alias="rubricNeedsReview", ge=0)
207
+ needs_review_notes: list[str] = Field(alias="needsReviewNotes")
208
+ scorecard: dict[str, Any] | None = None
209
+
210
+
211
+ __all__ = [
212
+ "AnalyzeFormFillOutput",
213
+ "ApplyBodyOpsOutput",
214
+ "ApplyEvalplanFillOutput",
215
+ "ApplyFormFillOutput",
216
+ "ApplyTableOpsOutput",
217
+ "CanonicalMixedFormAnalysisOutput",
218
+ "CanonicalMixedFormApplyOutput",
219
+ "LegacyFormFillAnalysisOutput",
220
+ "LegacyFormFillApplyOutput",
221
+ "LegacyFormFillVerifyOutput",
222
+ "VerifyFormFillOutput",
223
+ ]
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Cohesive plain-callable tool handlers."""
@@ -0,0 +1,377 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Shared automation handlers used by the optional MCP adapter."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import binascii
8
+ import copy
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any, Literal, cast
13
+
14
+ from ..office.authoring import (
15
+ inspect_document_authoring_quality as inspect_authoring_document_quality,
16
+ )
17
+ from hwpx.tools.id_integrity import check_id_integrity
18
+
19
+ from ..core.document import save_doc
20
+ from ..core.transactions import (
21
+ rotate_and_backup,
22
+ save_dry_run,
23
+ semantic_diff,
24
+ )
25
+ from ..document_state import document_state_payload, revision_mismatch_response
26
+ from .. import quality as quality_contract
27
+ from ..quality_generation import (
28
+ inspect_quality_fallback,
29
+ )
30
+ from ..storage import (
31
+ build_hwpx_verification_report,
32
+ )
33
+ from ..utils.helpers import resolve_path
34
+ from ..workflow.render_queue import DurableRenderQueue
35
+ from ..workflow.render_security import RenderSecurityPolicy
36
+ from ..workflow.render_transport import RemoteRenderClientV2
37
+ from ..workflow.rendering import NullRenderClientV2, QueueRenderClientV2
38
+ from ..runtime_services import RUNTIME_SERVICES
39
+
40
+
41
+ def _package_version(package: str) -> str:
42
+ return quality_contract.package_version(package)
43
+
44
+
45
+ def _env_float(name: str, default: float) -> float:
46
+ raw = os.environ.get(name)
47
+ if raw is None:
48
+ return default
49
+ try:
50
+ parsed = float(raw)
51
+ except ValueError:
52
+ return default
53
+ return max(0.1, parsed)
54
+
55
+
56
+ def _idempotency_scope(
57
+ tool_name: str, path: str, idempotency_key: str | None
58
+ ) -> str | None:
59
+ key = (idempotency_key or "").strip()
60
+ if not key:
61
+ return None
62
+ resolved = str(Path(path).resolve())
63
+ return f"{tool_name}:{resolved}:{key}"
64
+
65
+
66
+ def _idempotency_fingerprint(arguments: dict[str, Any]) -> str:
67
+ return json.dumps(arguments, ensure_ascii=False, sort_keys=True, default=str)
68
+
69
+
70
+ def _idempotency_replay(
71
+ scope: str | None,
72
+ *,
73
+ fingerprint: str,
74
+ ) -> dict[str, Any] | None:
75
+ if scope is None:
76
+ return None
77
+ cached = RUNTIME_SERVICES.idempotency_cache.get(scope)
78
+ if cached is None:
79
+ return None
80
+ if cached.get("fingerprint") != fingerprint:
81
+ raise ValueError("idempotency_key was reused with different arguments")
82
+ payload = copy.deepcopy(cached["payload"])
83
+ payload["idempotentReplay"] = True
84
+ return payload
85
+
86
+
87
+ def _idempotency_store(
88
+ scope: str | None,
89
+ *,
90
+ fingerprint: str,
91
+ payload: dict[str, Any],
92
+ ) -> dict[str, Any]:
93
+ if scope is None:
94
+ return payload
95
+ stored = copy.deepcopy(payload)
96
+ stored["idempotentReplay"] = False
97
+ RUNTIME_SERVICES.idempotency_cache[scope] = {
98
+ "fingerprint": fingerprint,
99
+ "payload": stored,
100
+ }
101
+ while (
102
+ len(RUNTIME_SERVICES.idempotency_cache)
103
+ > RUNTIME_SERVICES.max_idempotency_cache_entries
104
+ ):
105
+ RUNTIME_SERVICES.idempotency_cache.pop(
106
+ next(iter(RUNTIME_SERVICES.idempotency_cache))
107
+ )
108
+ return copy.deepcopy(stored)
109
+
110
+
111
+ def _normalize_fill_mappings(mappings: dict[str, str]) -> dict[str, str]:
112
+ if not isinstance(mappings, dict):
113
+ raise ValueError(
114
+ "mappings must be an object mapping path strings to text values"
115
+ )
116
+ if not mappings:
117
+ raise ValueError("mappings must not be empty")
118
+
119
+ normalized: dict[str, str] = {}
120
+ for path, value in mappings.items():
121
+ if not isinstance(path, str) or not path.strip():
122
+ raise ValueError("mappings keys must be non-empty strings")
123
+ normalized[path] = value if isinstance(value, str) else str(value)
124
+ return normalized
125
+
126
+
127
+ def _decode_image_base64(image_base64: str) -> bytes:
128
+ try:
129
+ payload = base64.b64decode((image_base64 or "").strip(), validate=True)
130
+ except (binascii.Error, ValueError) as exc:
131
+ raise ValueError("invalid image_base64 payload") from exc
132
+ if not payload:
133
+ raise ValueError("image_base64 decoded to empty payload")
134
+ return payload
135
+
136
+
137
+ def _id_integrity_payload(doc: Any) -> dict[str, Any]:
138
+ report = check_id_integrity(doc)
139
+ return {
140
+ "ok": report.ok,
141
+ "dangling": [str(item) for item in report.dangling],
142
+ "orphanBinData": [str(item) for item in report.orphan_bin_data],
143
+ "ignored": [str(item) for item in report.ignored],
144
+ }
145
+
146
+
147
+ def _save_doc_verification(
148
+ doc: Any, path: str, *, quality: Any = None
149
+ ) -> dict[str, Any]:
150
+ target = Path(path)
151
+ backup = rotate_and_backup(target)
152
+ verification = save_doc(doc, path, quality=quality)
153
+ if not isinstance(verification, dict):
154
+ verification = build_hwpx_verification_report(target)
155
+ verification["filePath"] = str(target)
156
+ verification["backup"] = backup.to_dict()
157
+ if backup.backup_path is not None:
158
+ try:
159
+ verification["semanticDiff"] = semantic_diff(backup.backup_path, target)
160
+ except Exception as exc: # pragma: no cover - diagnostic fallback
161
+ verification["semanticDiff"] = {
162
+ "schemaVersion": "hwpx.semantic-diff.v1",
163
+ "changed": True,
164
+ "summary": f"Semantic diff unavailable: {exc}",
165
+ "items": [],
166
+ "error": str(exc),
167
+ }
168
+ return verification
169
+
170
+
171
+ def _with_document_state(result: dict[str, Any], path: str) -> dict[str, Any]:
172
+ payload = dict(result)
173
+ payload.update(document_state_payload(path))
174
+ return payload
175
+
176
+
177
+ def _revision_guard(path: str, expected_revision: str | None) -> dict[str, Any] | None:
178
+ return revision_mismatch_response(path, expected_revision)
179
+
180
+
181
+ def _with_save_verification(
182
+ result: dict[str, Any], verification: dict[str, Any]
183
+ ) -> dict[str, Any]:
184
+ payload = dict(result)
185
+ payload.setdefault("dryRun", False)
186
+ payload["verificationReport"] = verification
187
+ payload.setdefault("openSafety", verification.get("openSafety"))
188
+ if "visualComplete" in verification:
189
+ payload.setdefault("visualComplete", verification["visualComplete"])
190
+ if "semanticDiff" in verification:
191
+ payload.setdefault("semanticDiff", verification["semanticDiff"])
192
+ if "backup" in verification:
193
+ payload.setdefault("backup", verification["backup"])
194
+ file_path = verification.get("filePath")
195
+ if isinstance(file_path, str):
196
+ payload.update(document_state_payload(file_path))
197
+ return payload
198
+
199
+
200
+ def _with_dry_run_verification(
201
+ result: dict[str, Any], doc: Any, path: str, *, quality: Any = None
202
+ ) -> dict[str, Any]:
203
+ payload = dict(result)
204
+ dry_run = save_dry_run(doc, path, quality=quality)
205
+ payload.update(dry_run)
206
+ payload.update(document_state_payload(path))
207
+ return payload
208
+
209
+
210
+ def _quality_profile_argument(
211
+ quality_profile: str | dict | None,
212
+ profile: dict | None = None,
213
+ ) -> str | dict | None:
214
+ """Normalize MCP quality-profile arguments for python-hwpx."""
215
+
216
+ if profile:
217
+ merged = dict(profile)
218
+ if quality_profile:
219
+ merged.setdefault("name", quality_profile)
220
+ return merged
221
+ return quality_profile
222
+
223
+
224
+ def _inspect_authoring_quality(
225
+ source: str | Any,
226
+ *,
227
+ document_plan: dict | None,
228
+ quality_profile: str | dict | None = None,
229
+ profile: dict | None = None,
230
+ verify_render: bool = False,
231
+ ) -> dict:
232
+ if inspect_authoring_document_quality is None:
233
+ raise RuntimeError(
234
+ "installed python-hwpx does not provide document-plan authoring"
235
+ )
236
+ profile_arg = _quality_profile_argument(quality_profile, profile)
237
+ kwargs: dict[str, Any] = {"plan": document_plan}
238
+ if profile_arg is not None:
239
+ kwargs["quality_profile"] = profile_arg
240
+ if verify_render:
241
+ kwargs["verify_render"] = True
242
+ try:
243
+ return inspect_authoring_document_quality(source, **kwargs)
244
+ except TypeError as exc:
245
+ # An older installed python-hwpx may predate verify_render and/or the
246
+ # quality_profile kwarg; retry without the unsupported argument(s).
247
+ if verify_render:
248
+ kwargs.pop("verify_render", None)
249
+ try:
250
+ return inspect_authoring_document_quality(source, **kwargs)
251
+ except TypeError:
252
+ pass
253
+ if profile_arg is not None:
254
+ raise RuntimeError(
255
+ "installed python-hwpx does not support document-plan quality profiles"
256
+ ) from exc
257
+ raise
258
+
259
+
260
+ def _diff_sources(
261
+ *,
262
+ old_filename: str | None = None,
263
+ new_filename: str | None = None,
264
+ old_paragraphs: list[str] | None = None,
265
+ new_paragraphs: list[str] | None = None,
266
+ ) -> tuple[Any, Any]:
267
+ if old_filename and new_filename:
268
+ return resolve_path(old_filename), resolve_path(new_filename)
269
+ if old_paragraphs is not None and new_paragraphs is not None:
270
+ return old_paragraphs, new_paragraphs
271
+ raise ValueError(
272
+ "provide old_filename/new_filename or old_paragraphs/new_paragraphs"
273
+ )
274
+
275
+
276
+ def _proposal_quality_fallback(path: str) -> dict:
277
+ """Compatibility report when installed python-hwpx lacks proposal presets."""
278
+
279
+ report = inspect_quality_fallback(path)
280
+ table_checks = dict(report.get("table_checks") or {})
281
+ table_checks.setdefault(
282
+ "has_budget_table", bool(table_checks.get("has_structured_tables"))
283
+ )
284
+ report["table_checks"] = table_checks
285
+ report["report_version"] = "proposal-quality-v2"
286
+ return report
287
+
288
+
289
+ def _capability_block(tool_surface_skew: bool, surface_details: list[str]) -> dict:
290
+ """Core/automation/plugin capability handshake (plan §2 Phase F).
291
+
292
+ Versions + a fingerprint hash + skew. Writes fail closed on a *version* skew
293
+ (the SavePipeline gate would otherwise be unavailable). Tool-surface skew is
294
+ part of the installed capability verdict and is never reported healthy.
295
+ """
296
+
297
+ state = quality_contract.capability_state()
298
+ skew = list(state["skew"])
299
+ if tool_surface_skew:
300
+ detail = "; ".join(surface_details) or "ToolSpec mismatch"
301
+ skew.append(f"MCP tool surface skew: {detail}")
302
+ fail_closed = quality_contract.fail_closed_enabled()
303
+ return {
304
+ "handshake": "hwpx.capability.v1",
305
+ "versions": state["versions"],
306
+ "minPythonHwpx": state["minPythonHwpx"],
307
+ "minAutomationVersion": state["minAutomationVersion"],
308
+ # 6.x compatibility alias for existing health consumers.
309
+ "minMcpVersion": state["minMcpVersion"],
310
+ "minSkillVersion": state["minSkillVersion"],
311
+ "savePipelineAvailable": state["savePipelineAvailable"],
312
+ "hash": state["hash"],
313
+ "toolContractHash": state["toolContractHash"],
314
+ "skew": skew,
315
+ "ok": not skew,
316
+ "failClosed": fail_closed,
317
+ "writesBlocked": fail_closed and bool(skew),
318
+ "diagnosis": (
319
+ "Capability handshake OK; general saves use SavePipeline, while guarded "
320
+ "byte-preserving form writers return open-safety receipts."
321
+ if not skew
322
+ else "Capability skew: install the contract-required core/automation/plugin versions and restart the host."
323
+ ),
324
+ }
325
+
326
+
327
+ def _render_client():
328
+ root = os.environ.get("HWPX_RENDER_QUEUE_ROOT")
329
+ remote_url = os.environ.get("HWPX_RENDER_QUEUE_URL")
330
+ secret = os.environ.get("HWPX_RENDER_QUEUE_SECRET")
331
+ if remote_url and secret:
332
+ transport_auth = os.environ.get("HWPX_RENDER_TRANSPORT_AUTH", "mtls")
333
+ ca_file = os.environ.get("HWPX_RENDER_CA_FILE")
334
+ client_certfile = os.environ.get("HWPX_RENDER_CLIENT_CERT_FILE")
335
+ client_keyfile = os.environ.get("HWPX_RENDER_CLIENT_KEY_FILE")
336
+ return RemoteRenderClientV2(
337
+ remote_url,
338
+ secret=secret.encode("utf-8"),
339
+ transport_auth=cast(Literal["mtls", "signed_https"], transport_auth),
340
+ ca_file=Path(ca_file).expanduser().resolve() if ca_file else None,
341
+ client_certfile=Path(client_certfile).expanduser().resolve()
342
+ if client_certfile
343
+ else None,
344
+ client_keyfile=Path(client_keyfile).expanduser().resolve()
345
+ if client_keyfile
346
+ else None,
347
+ )
348
+ if not root or not secret:
349
+ return NullRenderClientV2()
350
+ queue_root = Path(root).expanduser().resolve()
351
+ policy = RenderSecurityPolicy(sandbox_root=queue_root / "sandboxes")
352
+ queue = DurableRenderQueue(queue_root, secret=secret.encode("utf-8"), policy=policy)
353
+ return QueueRenderClientV2(queue, secret=secret.encode("utf-8"))
354
+
355
+
356
+ __all__ = [
357
+ "_revision_guard",
358
+ "_save_doc_verification",
359
+ "_with_document_state",
360
+ "_with_dry_run_verification",
361
+ "_with_save_verification",
362
+ "_env_float",
363
+ "_capability_block",
364
+ "_decode_image_base64",
365
+ "_diff_sources",
366
+ "_id_integrity_payload",
367
+ "_idempotency_fingerprint",
368
+ "_idempotency_replay",
369
+ "_idempotency_scope",
370
+ "_idempotency_store",
371
+ "_inspect_authoring_quality",
372
+ "_normalize_fill_mappings",
373
+ "_package_version",
374
+ "_proposal_quality_fallback",
375
+ "_quality_profile_argument",
376
+ "_render_client",
377
+ ]