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,982 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Typed, deterministic contract for the installed FastMCP tool surface.
3
+
4
+ ``BASELINE_TOOL_SPECS`` records the current classification baseline, including
5
+ the fixture-QA tools that became internal in 4.0.0 (the 3.0.0 census minus the
6
+ practice tools removed in 4.0.0 and the transition stubs removed at the 5.0.0
7
+ major boundary). ``TOOL_SPECS`` is the only installed/public surface.
8
+ Registration, capability reporting, generated documentation, health checks, and
9
+ the versioned contract delta all consume these objects instead of maintaining
10
+ parallel name lists.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import functools
16
+ import hashlib
17
+ import inspect
18
+ import json
19
+ from dataclasses import dataclass
20
+ from enum import Enum
21
+ from typing import Any, Callable, Mapping
22
+
23
+ MIN_PYTHON_HWPX = "5.0.0"
24
+ # The 5.0 train: core drops the workflow surfaces and the `hwpx` console name,
25
+ # this package picks the name up, and the plugin pins both. The three move
26
+ # together — a mixed set is what "no valid install has two declarers" rules out.
27
+ MIN_AUTOMATION_VERSION = "6.0.0"
28
+ # Compatibility alias retained in the 6.x contract payload for existing health
29
+ # and plugin consumers. New code and documentation use the automation name.
30
+ MIN_MCP_VERSION = MIN_AUTOMATION_VERSION
31
+ MIN_SKILL_VERSION = "1.0.0"
32
+ # Frozen release receipt for non-runtime services. Runtime construction still
33
+ # recomputes and verifies the bound callable/schema contract through
34
+ # ``contract_hash()``; this constant prevents those services from importing the
35
+ # runtime composer merely to stamp the approved release receipt.
36
+ RELEASED_CONTRACT_HASH = "0ce938371f0b55a6"
37
+
38
+
39
+ def describe_callables(entries: Any) -> Any:
40
+ """Load the optional FastMCP schema adapter only for explicit binding."""
41
+
42
+ from .fastmcp_adapter import describe_callables as implementation
43
+
44
+ return implementation(entries)
45
+
46
+
47
+ def register_canonical_tool(*args: Any, **kwargs: Any) -> Any:
48
+ """Load the optional FastMCP registrar only for explicit registration."""
49
+
50
+ from .fastmcp_adapter import register_canonical_tool as implementation
51
+
52
+ return implementation(*args, **kwargs)
53
+
54
+
55
+ def snapshot_runtime_tools(mcp: Any) -> Any:
56
+ """Load the optional FastMCP snapshot adapter only for runtime validation."""
57
+
58
+ from .fastmcp_adapter import snapshot_runtime_tools as implementation
59
+
60
+ return implementation(mcp)
61
+
62
+
63
+ class ToolClassification(str, Enum):
64
+ """Product decision for one name in the 131-tool classification baseline."""
65
+
66
+ PUBLIC = "public"
67
+ COMPATIBILITY = "compatibility"
68
+ ADVANCED = "advanced"
69
+ DEPRECATED = "deprecated"
70
+ INTERNAL = "internal"
71
+
72
+
73
+ class ToolProfile(str, Enum):
74
+ DEFAULT = "default"
75
+ ADVANCED = "advanced"
76
+
77
+
78
+ class ToolAvailability(str, Enum):
79
+ """Declared registration state of a tool in the selected profile."""
80
+
81
+ AVAILABLE = "available"
82
+ PROFILE_GATED = "profile-gated"
83
+ INTERNAL_ONLY = "internal-only"
84
+ UNAVAILABLE = "unavailable"
85
+
86
+
87
+ class AvailabilityReasonCode(str, Enum):
88
+ CALLABLE_MISSING = "CALLABLE_MISSING"
89
+ DEPENDENCY_MISSING = "DEPENDENCY_MISSING"
90
+ DEPENDENCY_VERSION_SKEW = "DEPENDENCY_VERSION_SKEW"
91
+ PROFILE_DISABLED = "PROFILE_DISABLED"
92
+ INTERNAL_ONLY = "INTERNAL_ONLY"
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class AvailabilityRequirement:
97
+ dependency: str
98
+ minimum_version: str
99
+ optional: bool = False
100
+ extra: str | None = None
101
+
102
+
103
+ @dataclass(frozen=True, slots=True)
104
+ class AvailabilityReason:
105
+ code: AvailabilityReasonCode
106
+ message: str
107
+ dependency: str | None = None
108
+ required_version: str | None = None
109
+ installed_version: str | None = None
110
+ install_hint: str | None = None
111
+
112
+
113
+ class SchemaBinding(str, Enum):
114
+ """Authoritative source from which FastMCP builds the input schema."""
115
+
116
+ PYTHON_SIGNATURE = "python-signature"
117
+ INTERNAL_LIBRARY = "internal-library"
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class DomainSpec:
122
+ key: str
123
+ title: str
124
+ intent: str
125
+ when_to_use: str
126
+ tools: tuple[str, ...]
127
+ public: bool = True
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class ToolSpec:
132
+ """Static release decision plus explicit callable/schema binding."""
133
+
134
+ name: str
135
+ domain: str
136
+ profile: ToolProfile
137
+ classification: ToolClassification
138
+ availability_requirement: AvailabilityRequirement
139
+ availability: ToolAvailability
140
+ availability_reason: AvailabilityReason | None
141
+ reason: str
142
+ replacement_tools: tuple[str, ...] = ()
143
+ skill_required: bool = False
144
+ callable_name: str | None = None
145
+ schema_binding: SchemaBinding = SchemaBinding.PYTHON_SIGNATURE
146
+ mutates: bool = False
147
+ tags: tuple[str, ...] = ()
148
+
149
+ @property
150
+ def installed(self) -> bool:
151
+ return self.classification is not ToolClassification.INTERNAL
152
+
153
+
154
+ @dataclass(frozen=True, slots=True)
155
+ class SchemaParameter:
156
+ name: str
157
+ annotation: str
158
+ required: bool
159
+
160
+
161
+ @dataclass(frozen=True, slots=True)
162
+ class BoundToolSpec:
163
+ """A ToolSpec resolved to one concrete callable before registration."""
164
+
165
+ spec: ToolSpec
166
+ function: Callable[..., Any]
167
+ signature: str
168
+ parameters: tuple[SchemaParameter, ...]
169
+ description: str
170
+ input_schema: Mapping[str, Any]
171
+ output_schema: Mapping[str, Any]
172
+ availability: ToolAvailability = ToolAvailability.AVAILABLE
173
+ availability_reason: AvailabilityReason | None = None
174
+
175
+
176
+ @dataclass(frozen=True, slots=True)
177
+ class RegisteredToolRegistry:
178
+ """Immutable record of the exact callables handed to FastMCP."""
179
+
180
+ tools: tuple[BoundToolSpec, ...]
181
+
182
+ @property
183
+ def names(self) -> tuple[str, ...]:
184
+ return tuple(item.spec.name for item in self.tools)
185
+
186
+ def callable_map(self) -> Mapping[str, Callable[..., Any]]:
187
+ return {item.spec.name: item.function for item in self.tools}
188
+
189
+ def by_name(self) -> Mapping[str, BoundToolSpec]:
190
+ return {item.spec.name: item for item in self.tools}
191
+
192
+ def payload(self) -> dict[str, Any]:
193
+ return {
194
+ "schemaVersion": "hwpx.bound-tool-registry.v1",
195
+ "contractHash": contract_hash(),
196
+ "tools": [
197
+ {
198
+ "name": item.spec.name,
199
+ "callableName": item.spec.callable_name,
200
+ "signature": item.signature,
201
+ "description": item.description,
202
+ "inputSchema": item.input_schema,
203
+ "outputSchema": item.output_schema,
204
+ "availability": item.availability.value,
205
+ "availabilityReason": _availability_reason_payload(
206
+ item.availability_reason
207
+ ),
208
+ "parameters": [
209
+ {
210
+ "name": parameter.name,
211
+ "annotation": parameter.annotation,
212
+ "required": parameter.required,
213
+ }
214
+ for parameter in item.parameters
215
+ ],
216
+ }
217
+ for item in self.tools
218
+ ],
219
+ }
220
+
221
+ def binding_hash(self) -> str:
222
+ raw = json.dumps(self.payload(), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
223
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
224
+
225
+
226
+ _ADVANCED_CLASSIFICATION = {
227
+ "package_parts",
228
+ "package_get_xml",
229
+ "package_get_text",
230
+ "object_find_by_tag",
231
+ "object_find_by_attr",
232
+ "validate_structure",
233
+ "lint_text_conventions",
234
+ "score_form_fill",
235
+ }
236
+
237
+ _ADVANCED_PROFILE = _ADVANCED_CLASSIFICATION
238
+
239
+ _COMPATIBILITY_REPLACEMENTS: dict[str, tuple[str, ...]] = {
240
+ "apply_edits": ("apply_document_commands",),
241
+ "apply_evalplan_fill": ("analyze_form_fill", "apply_form_fill", "verify_form_fill"),
242
+ "create_comparison_table_document": ("create_document_from_plan",),
243
+ "create_government_report_document": ("create_document_from_plan",),
244
+ "create_proposal_document": ("create_document_from_plan",),
245
+ "fill_by_path": ("analyze_form_fill", "apply_form_fill", "verify_form_fill"),
246
+ }
247
+
248
+ # At the 5.0.0 major boundary, the three template-formfit facades demote from
249
+ # COMPATIBILITY to DEPRECATED. Their handlers and behaviour are unchanged; they
250
+ # now carry the one-transition deprecation guidance toward the canonical
251
+ # analyze/apply/verify_form_fill trio, exactly like the former stub group.
252
+ _DEPRECATED_REPLACEMENTS: dict[str, tuple[str, ...]] = {
253
+ "analyze_template_formfit": ("analyze_form_fill", "apply_form_fill", "verify_form_fill"),
254
+ "apply_template_formfit": ("analyze_form_fill", "apply_form_fill", "verify_form_fill"),
255
+ "fill_form_field": ("analyze_form_fill", "apply_form_fill", "verify_form_fill"),
256
+ }
257
+
258
+ _INTERNAL_QA_TOOLS = {
259
+ "export_fixture_benchmark",
260
+ "run_fixture_benchmark",
261
+ "visual_repair_fixture",
262
+ "visual_review_fixture",
263
+ }
264
+
265
+ _SKILL_REQUIRED_TOOLS = {
266
+ "get_document_node",
267
+ "query_document_nodes",
268
+ "apply_document_commands",
269
+ "dump_document_blueprint",
270
+ "replay_document_blueprint",
271
+ "create_document_from_plan",
272
+ "create_government_report_document",
273
+ "mail_merge",
274
+ "table_compute",
275
+ "extract_style_profile",
276
+ "list_templates",
277
+ "get_document_map",
278
+ "repair_hwpx",
279
+ "replace_by_anchor",
280
+ "add_memo_by_anchor",
281
+ "byte_preserving_patch",
282
+ "render_preview",
283
+ "apply_edits",
284
+ "add_tracked_edit",
285
+ "undo_last_edit",
286
+ "compose_exam",
287
+ "scan_form_guidance",
288
+ "apply_table_ops",
289
+ "apply_body_ops",
290
+ "inspect_fill_residue",
291
+ "verify_form_fill",
292
+ "apply_evalplan_fill",
293
+ "score_form_fill",
294
+ }
295
+
296
+
297
+ _MUTATING_TOOLS = {
298
+ "apply_document_commands", "replay_document_blueprint",
299
+ # These render tools can materialize queue or preview artifacts when their
300
+ # optional output arguments are supplied, so classify them conservatively
301
+ # as writers for process-wide serialization.
302
+ "render_preview", "render_submit", "render_status", "render_cancel",
303
+ "start_workflow", "continue_workflow", "approve_workflow_decision",
304
+ "cancel_workflow", "resume_workflow",
305
+ "apply_table_ops", "apply_body_ops", "fill_form_field", "fill_by_path",
306
+ "apply_form_fill", "apply_template_formfit",
307
+ "apply_evalplan_fill",
308
+ "create_document", "create_document_from_plan", "copy_document",
309
+ "create_government_report_document", "create_proposal_document",
310
+ "create_comparison_table_document", "register_template",
311
+ "add_heading", "add_paragraph", "insert_paragraph", "delete_paragraph",
312
+ "add_page_break", "apply_edits", "undo_last_edit",
313
+ "replace_by_anchor", "replace_in_paragraph", "search_and_replace", "batch_replace",
314
+ "byte_preserving_patch", "insert_picture", "replace_picture",
315
+ "add_table", "set_table_cell_text", "merge_table_cells", "split_table_cell",
316
+ "format_table", "table_compute",
317
+ "create_custom_style", "set_paragraph_format", "set_list_format", "format_text",
318
+ "apply_style_profile_to_plan", "set_page_setup", "set_header_footer",
319
+ "set_page_number", "add_toc", "add_cross_reference", "add_tracked_edit",
320
+ "compose_exam", "place_seal", "mail_merge", "build_image_grid",
321
+ "build_meeting_nameplates", "build_organization_chart", "add_memo",
322
+ "add_memo_by_anchor", "remove_memo", "repair_hwpx",
323
+ }
324
+
325
+
326
+ BASELINE_DOMAIN_SPECS: tuple[DomainSpec, ...] = (
327
+ DomainSpec(
328
+ "agent_document",
329
+ "에이전트 문서 인터페이스",
330
+ "공유 semantic node/path/query/command 계약으로 낯선 HWPX를 탐색하고 이종 편집을 한 번에 원자 적용.",
331
+ "get_document_node/query_document_nodes로 revision-bound canonical path를 찾은 뒤 "
332
+ "apply_document_commands 한 배치로 적용한다. 도메인 증거가 필요한 작업은 전문 도구를 사용한다.",
333
+ (
334
+ "get_document_node",
335
+ "query_document_nodes",
336
+ "apply_document_commands",
337
+ "dump_document_blueprint",
338
+ "replay_document_blueprint",
339
+ ),
340
+ ),
341
+ DomainSpec(
342
+ "real_hancom_render",
343
+ "실한컴 비동기 렌더",
344
+ "인증된 private queue로 HWPX를 제출하고 실한컴 PDF·페이지 영수증을 비동기로 조회.",
345
+ "render_submit 후 job id를 받고 render_status로 폴링한다. unavailable/degraded이면 unverified로 보류한다.",
346
+ ("render_submit", "render_status", "render_cancel", "render_health"),
347
+ ),
348
+ DomainSpec(
349
+ "workflow",
350
+ "자율 문서 워크플로",
351
+ "서버가 상태·정책·결정·예산·검증을 강제하는 재시작 가능한 고수준 문서 작업.",
352
+ "호스트별 스킬 지식 없이 HWPX 작업을 안전하게 시작·진행·승인·재개할 때.",
353
+ (
354
+ "start_workflow",
355
+ "get_workflow",
356
+ "get_workflow_result",
357
+ "continue_workflow",
358
+ "approve_workflow_decision",
359
+ "cancel_workflow",
360
+ "resume_workflow",
361
+ ),
362
+ ),
363
+ DomainSpec(
364
+ "visual_qa",
365
+ "전 페이지 fixture 비전 검수",
366
+ "제품 QA fixture 전용 검수와 제한 수리.",
367
+ "설치형 제품 도구가 아니라 CI 라이브러리로만 실행한다.",
368
+ ("visual_review_fixture", "visual_repair_fixture"),
369
+ public=False,
370
+ ),
371
+ DomainSpec(
372
+ "blind_eval",
373
+ "블라인드 fixture 실무 평가",
374
+ "동결 fixture 실행·판정 증거와 익명 심사용 번들.",
375
+ "설치형 제품 도구가 아니라 CI 라이브러리로만 실행한다.",
376
+ ("run_fixture_benchmark", "export_fixture_benchmark"),
377
+ public=False,
378
+ ),
379
+ DomainSpec(
380
+ "read",
381
+ "읽기·추출·변환",
382
+ "HWPX 문서를 읽어 텍스트·구조·서식을 뽑거나 Markdown/HTML/JSON으로 변환.",
383
+ "문서 내용을 이해·인용·재작업하거나 다른 형식으로 내보낼 때.",
384
+ (
385
+ "get_document_text", "get_document_info", "get_document_outline", "get_document_map",
386
+ "get_paragraph_text", "get_paragraphs_text", "get_location_text", "get_table_text",
387
+ "get_table_map", "find_text", "list_available_documents", "hwpx_to_markdown",
388
+ "document_to_markdown", "hwpx_to_html", "hwpx_extract_json", "document_extract_json",
389
+ ),
390
+ ),
391
+ DomainSpec(
392
+ "form_fill",
393
+ "양식 채움 (동적)",
394
+ "처음 보는 실양식을 정찰하고 typed plan을 한 transaction으로 채운 뒤 검증.",
395
+ "analyze_form_fill → 승인 → apply_form_fill → verify_form_fill을 canonical 경로로 사용한다.",
396
+ (
397
+ "scan_form_guidance", "apply_table_ops", "apply_body_ops", "inspect_fill_residue",
398
+ "verify_form_fill", "list_form_fields", "fill_form_field", "find_cell_by_label",
399
+ "fill_by_path", "analyze_form_fill", "apply_form_fill", "analyze_template_formfit",
400
+ "apply_template_formfit",
401
+ "apply_evalplan_fill", "score_form_fill",
402
+ ),
403
+ ),
404
+ DomainSpec(
405
+ "author",
406
+ "문서 생성",
407
+ "계획·브리프로 새 HWPX 문서를 생성.",
408
+ "빈 문서에서 공문·보고서·제안서·비교표 등을 저작할 때.",
409
+ (
410
+ "create_document", "create_document_from_plan", "copy_document",
411
+ "create_government_report_document", "create_proposal_document",
412
+ "create_comparison_table_document", "get_document_plan_schema", "validate_document_plan",
413
+ "analyze_document_plan", "markdown_to_document_plan", "parse_government_report_text",
414
+ "compute_report_value", "register_template", "list_templates", "describe_template",
415
+ ),
416
+ ),
417
+ DomainSpec(
418
+ "edit",
419
+ "편집",
420
+ "기존 문서의 문단·그림·텍스트를 안전하게 편집.",
421
+ "기존 문서를 고치거나 byte-preserving 수술을 적용할 때.",
422
+ (
423
+ "add_heading", "add_paragraph", "insert_paragraph", "delete_paragraph", "add_page_break",
424
+ "apply_edits", "undo_last_edit",
425
+ "replace_by_anchor", "replace_in_paragraph", "search_and_replace", "batch_replace",
426
+ "byte_preserving_patch", "insert_picture", "replace_picture",
427
+ ),
428
+ ),
429
+ DomainSpec(
430
+ "tables", "표 조작", "표 생성·셀 편집·병합·계산.", "표를 직접 만들거나 값을 바꿀 때.",
431
+ ("add_table", "set_table_cell_text", "merge_table_cells", "split_table_cell", "format_table", "table_compute"),
432
+ ),
433
+ DomainSpec(
434
+ "styles", "서식·스타일", "문단/런 서식과 스타일 프로파일을 편집.", "글꼴·정렬·목록·서식을 바꿀 때.",
435
+ (
436
+ "list_styles", "create_custom_style", "set_paragraph_format", "set_list_format",
437
+ "format_text", "extract_style_profile", "apply_style_profile_to_plan", "compare_style_profiles",
438
+ ),
439
+ ),
440
+ DomainSpec(
441
+ "layout", "페이지·머리글·쪽번호", "페이지 레이아웃 요소를 설정.", "용지·머리글·쪽번호를 바꿀 때.",
442
+ ("set_page_setup", "set_header_footer", "set_page_number"),
443
+ ),
444
+ DomainSpec(
445
+ "toc_xref", "차례·상호참조", "한컴 네이티브 차례와 쪽 상호참조를 저작·검증.",
446
+ "자동 목차/상호참조가 필요할 때.", ("add_toc", "add_cross_reference", "verify_toc"),
447
+ ),
448
+ DomainSpec("pii", "개인정보", "개인정보를 탐지·마스킹.", "배포 전 PII를 감사할 때.", ("scan_personal_info",)),
449
+ DomainSpec("redline", "변경추적", "수락/거부 가능한 추적 변경을 저작.", "검토용 redline이 필요할 때.", ("add_tracked_edit",)),
450
+ DomainSpec("exam", "시험지 조판", "문항 Markdown을 학교 양식에 조판.", "시험지 원안지에 문항을 앉힐 때.", ("compose_exam", "verify_question_splits")),
451
+ DomainSpec("seal", "직인·관인", "직인을 배치하고 규정을 검사.", "공문 직인 처리에.", ("place_seal", "check_seal_compliance")),
452
+ DomainSpec(
453
+ "generators", "대량생산·특수 산출", "메일머지·사진대지·명패·조직도를 생성.",
454
+ "반복 문서나 특수 레이아웃을 만들 때.",
455
+ ("mail_merge", "inspect_mail_merge_placeholders", "build_image_grid", "build_meeting_nameplates", "build_organization_chart"),
456
+ ),
457
+ DomainSpec("memo", "메모·주석", "검토 메모를 추가·삭제.", "문서에 코멘트를 달 때.", ("add_memo", "add_memo_by_anchor", "remove_memo")),
458
+ DomainSpec(
459
+ "verify_quality", "검증·품질·복구", "렌더·품질·정합·복구·서버 상태를 검사.",
460
+ "산출물 확언 전 검증하거나 문제를 진단할 때.",
461
+ (
462
+ "render_preview", "repair_hwpx", "mcp_server_health", "describe_capabilities", "doc_diff",
463
+ "validate_structure", "lint_text_conventions", "inspect_document_quality",
464
+ "inspect_document_authoring_quality", "inspect_operating_plan_quality",
465
+ "inspect_official_document_style", "inspect_reference_consistency",
466
+ ),
467
+ ),
468
+ DomainSpec(
469
+ "package_io", "패키지 조회", "고급 OPC 파트와 XML 객체를 조회.", "저수준 진단이 필요할 때.",
470
+ ("package_parts", "package_get_text", "package_get_xml", "object_find_by_attr", "object_find_by_tag"),
471
+ ),
472
+ )
473
+
474
+ DOMAIN_SPECS: tuple[DomainSpec, ...] = tuple(domain for domain in BASELINE_DOMAIN_SPECS if domain.public)
475
+
476
+
477
+ def _classification(name: str) -> ToolClassification:
478
+ if name in _INTERNAL_QA_TOOLS:
479
+ return ToolClassification.INTERNAL
480
+ if name in _DEPRECATED_REPLACEMENTS:
481
+ return ToolClassification.DEPRECATED
482
+ if name in _COMPATIBILITY_REPLACEMENTS:
483
+ return ToolClassification.COMPATIBILITY
484
+ if name in _ADVANCED_CLASSIFICATION:
485
+ return ToolClassification.ADVANCED
486
+ return ToolClassification.PUBLIC
487
+
488
+
489
+ def _reason(classification: ToolClassification) -> str:
490
+ return {
491
+ ToolClassification.PUBLIC: "canonical installed product capability",
492
+ ToolClassification.COMPATIBILITY: "transition facade over a canonical typed engine",
493
+ ToolClassification.ADVANCED: "opt-in diagnostic or expert capability outside ordinary routing",
494
+ ToolClassification.DEPRECATED: "one-transition stub with explicit replacement guidance",
495
+ ToolClassification.INTERNAL: "fixture QA library retained for CI but removed from installed registration",
496
+ }[classification]
497
+
498
+
499
+ def _build_baseline_tool_specs() -> tuple[ToolSpec, ...]:
500
+ seen: set[str] = set()
501
+ specs: list[ToolSpec] = []
502
+ for domain in BASELINE_DOMAIN_SPECS:
503
+ for name in domain.tools:
504
+ if name in seen:
505
+ raise RuntimeError(f"duplicate ToolSpec name: {name}")
506
+ seen.add(name)
507
+ classification = _classification(name)
508
+ internal = classification is ToolClassification.INTERNAL
509
+ profile = ToolProfile.ADVANCED if name in _ADVANCED_PROFILE else ToolProfile.DEFAULT
510
+ availability = (
511
+ ToolAvailability.INTERNAL_ONLY
512
+ if internal
513
+ else ToolAvailability.PROFILE_GATED
514
+ if profile is ToolProfile.ADVANCED
515
+ else ToolAvailability.AVAILABLE
516
+ )
517
+ availability_reason = (
518
+ AvailabilityReason(
519
+ code=AvailabilityReasonCode.INTERNAL_ONLY,
520
+ message="fixture QA capability is retained only as an internal library",
521
+ )
522
+ if internal
523
+ else AvailabilityReason(
524
+ code=AvailabilityReasonCode.PROFILE_DISABLED,
525
+ message=(
526
+ "set HWPX_AUTOMATION_ADVANCED=1 before server import "
527
+ "(HWPX_MCP_ADVANCED remains a 6.x fallback)"
528
+ ),
529
+ )
530
+ if profile is ToolProfile.ADVANCED
531
+ else None
532
+ )
533
+ tags = tuple(
534
+ tag
535
+ for tag, enabled in (
536
+ (domain.key, True),
537
+ (classification.value, True),
538
+ ("mutation", name in _MUTATING_TOOLS),
539
+ ("skill-required", name in _SKILL_REQUIRED_TOOLS),
540
+ )
541
+ if enabled
542
+ )
543
+ specs.append(
544
+ ToolSpec(
545
+ name=name,
546
+ domain=domain.key,
547
+ profile=profile,
548
+ classification=classification,
549
+ availability_requirement=AvailabilityRequirement(
550
+ dependency="python-hwpx",
551
+ minimum_version=MIN_PYTHON_HWPX,
552
+ ),
553
+ availability=availability,
554
+ availability_reason=availability_reason,
555
+ reason=_reason(classification),
556
+ replacement_tools=(
557
+ _COMPATIBILITY_REPLACEMENTS.get(name)
558
+ or _DEPRECATED_REPLACEMENTS.get(name)
559
+ or ()
560
+ ),
561
+ skill_required=name in _SKILL_REQUIRED_TOOLS,
562
+ callable_name=None if internal else name,
563
+ schema_binding=(
564
+ SchemaBinding.INTERNAL_LIBRARY if internal else SchemaBinding.PYTHON_SIGNATURE
565
+ ),
566
+ mutates=name in _MUTATING_TOOLS,
567
+ tags=tags,
568
+ )
569
+ )
570
+
571
+ expected_sets = (
572
+ _ADVANCED_CLASSIFICATION,
573
+ set(_COMPATIBILITY_REPLACEMENTS),
574
+ set(_DEPRECATED_REPLACEMENTS),
575
+ _INTERNAL_QA_TOOLS,
576
+ _SKILL_REQUIRED_TOOLS,
577
+ )
578
+ missing = set().union(*(items - seen for items in expected_sets))
579
+ if missing:
580
+ raise RuntimeError(f"classified tools absent from domains: {sorted(missing)}")
581
+ return tuple(specs)
582
+
583
+
584
+ BASELINE_TOOL_SPECS = _build_baseline_tool_specs()
585
+ TOOL_SPECS = tuple(spec for spec in BASELINE_TOOL_SPECS if spec.installed)
586
+
587
+
588
+ def _validate_classification() -> None:
589
+ counts = classification_counts()
590
+ expected = {
591
+ ToolClassification.PUBLIC.value: 110,
592
+ ToolClassification.COMPATIBILITY.value: 6,
593
+ ToolClassification.ADVANCED.value: 8,
594
+ ToolClassification.DEPRECATED.value: 3,
595
+ ToolClassification.INTERNAL.value: 4,
596
+ }
597
+ if len(BASELINE_TOOL_SPECS) != 131 or counts != expected:
598
+ raise RuntimeError(
599
+ f"131-tool classification must be disjoint and exhaustive: {counts!r} != {expected!r}"
600
+ )
601
+ if len(TOOL_SPECS) != 127:
602
+ raise RuntimeError(f"installed advanced surface must contain 127 tools, got {len(TOOL_SPECS)}")
603
+ if sum(spec.skill_required for spec in TOOL_SPECS) != 28:
604
+ raise RuntimeError(
605
+ "installed surface must contain exactly 28 skill-required tools"
606
+ )
607
+
608
+
609
+ def classification_counts() -> dict[str, int]:
610
+ return {
611
+ classification.value: sum(
612
+ spec.classification is classification for spec in BASELINE_TOOL_SPECS
613
+ )
614
+ for classification in ToolClassification
615
+ }
616
+
617
+
618
+ _validate_classification()
619
+
620
+
621
+ def active_tool_specs(*, advanced: bool) -> tuple[ToolSpec, ...]:
622
+ return tuple(
623
+ spec
624
+ for spec in TOOL_SPECS
625
+ if spec.profile is ToolProfile.DEFAULT or advanced
626
+ )
627
+
628
+
629
+ def expected_tool_order(*, advanced: bool) -> tuple[str, ...]:
630
+ return tuple(spec.name for spec in active_tool_specs(advanced=advanced))
631
+
632
+
633
+ def expected_tool_names(*, advanced: bool) -> set[str]:
634
+ return set(expected_tool_order(advanced=advanced))
635
+
636
+
637
+ def skill_required_tool_names() -> set[str]:
638
+ return {spec.name for spec in TOOL_SPECS if spec.skill_required}
639
+
640
+
641
+ def classification_payload() -> dict[str, Any]:
642
+ return {
643
+ "schemaVersion": "hwpx.tool-classification.v1",
644
+ "baselineToolCount": len(BASELINE_TOOL_SPECS),
645
+ "counts": classification_counts(),
646
+ "tools": [_tool_payload(spec) for spec in BASELINE_TOOL_SPECS],
647
+ }
648
+
649
+
650
+ def _availability_reason_payload(reason: AvailabilityReason | None) -> dict[str, Any] | None:
651
+ if reason is None:
652
+ return None
653
+ return {
654
+ "code": reason.code.value,
655
+ "message": reason.message,
656
+ "dependency": reason.dependency,
657
+ "requiredVersion": reason.required_version,
658
+ "installedVersion": reason.installed_version,
659
+ "installHint": reason.install_hint,
660
+ }
661
+
662
+
663
+ def _tool_payload(spec: ToolSpec, bound: BoundToolSpec | None = None) -> dict[str, Any]:
664
+ payload: dict[str, Any] = {
665
+ "name": spec.name,
666
+ "domain": spec.domain,
667
+ "profile": spec.profile.value,
668
+ "classification": spec.classification.value,
669
+ "lifecycle": spec.classification.value,
670
+ "availabilityRequirement": {
671
+ "dependency": spec.availability_requirement.dependency,
672
+ "minimumVersion": spec.availability_requirement.minimum_version,
673
+ "optional": spec.availability_requirement.optional,
674
+ "extra": spec.availability_requirement.extra,
675
+ },
676
+ "declaredAvailability": spec.availability.value,
677
+ "availability": (bound.availability if bound else spec.availability).value,
678
+ "availabilityReason": _availability_reason_payload(
679
+ bound.availability_reason if bound else spec.availability_reason
680
+ ),
681
+ "decisionReason": spec.reason,
682
+ "replacementTools": list(spec.replacement_tools),
683
+ "skillRequired": spec.skill_required,
684
+ "callableName": spec.callable_name,
685
+ "schemaBinding": spec.schema_binding.value,
686
+ "mutates": spec.mutates,
687
+ "tags": list(spec.tags),
688
+ }
689
+ if bound is not None:
690
+ payload.update(
691
+ {
692
+ "description": bound.description,
693
+ "signature": bound.signature,
694
+ "inputSchema": bound.input_schema,
695
+ "outputSchema": bound.output_schema,
696
+ }
697
+ )
698
+ return payload
699
+
700
+
701
+ _BOUND_TOOL_REGISTRY: RegisteredToolRegistry | None = None
702
+
703
+
704
+ def bound_tool_registry() -> RegisteredToolRegistry:
705
+ """Return the complete installed registry after explicit runtime initialization."""
706
+
707
+ if _BOUND_TOOL_REGISTRY is None:
708
+ raise RuntimeError(
709
+ "canonical ToolSpec registry is not bound; initialize "
710
+ "hwpx_automation.runtime first"
711
+ )
712
+ return _BOUND_TOOL_REGISTRY
713
+
714
+
715
+ def contract_payload() -> dict[str, Any]:
716
+ registry = bound_tool_registry()
717
+ bound = registry.by_name()
718
+ return {
719
+ "schemaVersion": "hwpx.tool-contract.v2",
720
+ "minPythonHwpx": MIN_PYTHON_HWPX,
721
+ "minAutomationVersion": MIN_AUTOMATION_VERSION,
722
+ "minMcpVersion": MIN_MCP_VERSION,
723
+ "minSkillVersion": MIN_SKILL_VERSION,
724
+ "tools": [_tool_payload(spec, bound[spec.name]) for spec in TOOL_SPECS],
725
+ "baselineClassification": classification_payload(),
726
+ }
727
+
728
+
729
+ def contract_hash() -> str:
730
+ # Non-MCP automation services use the frozen release receipt without
731
+ # importing or constructing FastMCP. Once the optional adapter binds the
732
+ # canonical registry, recompute from live schemas and fail tests on drift.
733
+ if _BOUND_TOOL_REGISTRY is None:
734
+ return RELEASED_CONTRACT_HASH
735
+ raw = json.dumps(contract_payload(), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
736
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
737
+
738
+
739
+ def _annotation_name(annotation: Any) -> str:
740
+ if annotation is inspect.Parameter.empty:
741
+ return "untyped"
742
+ if isinstance(annotation, str):
743
+ return annotation
744
+ return getattr(annotation, "__name__", str(annotation).replace("typing.", ""))
745
+
746
+
747
+ def bind_tool_specs(
748
+ namespace: Mapping[str, Any],
749
+ *,
750
+ advanced: bool | None = None,
751
+ ) -> RegisteredToolRegistry:
752
+ """Resolve callable identity and normalized schemas before registration.
753
+
754
+ Every missing/non-callable binding and every schema-signature failure is
755
+ reported together, so import/programming errors cannot produce a silently
756
+ degraded partial registry.
757
+ """
758
+
759
+ # Keep the static ToolSpec/classification contract importable in the base
760
+ # automation distribution. FastMCP is an optional adapter and is loaded
761
+ # only when a caller explicitly binds the MCP schema surface.
762
+ specs = TOOL_SPECS if advanced is None else active_tool_specs(advanced=advanced)
763
+ resolved: list[tuple[ToolSpec, Callable[..., Any], inspect.Signature]] = []
764
+ errors: list[str] = []
765
+ schema_entries: list[tuple[str, Callable[..., Any], str, Mapping[str, Any]]] = []
766
+ for spec in specs:
767
+ callable_name = spec.callable_name
768
+ function = namespace.get(callable_name) if callable_name else None
769
+ if not callable(function):
770
+ errors.append(f"{spec.name}: missing callable {callable_name!r}")
771
+ continue
772
+ try:
773
+ signature = inspect.signature(function)
774
+ except (TypeError, ValueError) as exc:
775
+ errors.append(f"{spec.name}: invalid Python signature: {exc}")
776
+ continue
777
+ description = inspect.getdoc(function) or f"HWPX tool {spec.name}"
778
+ meta = {
779
+ "hwpxLifecycle": spec.classification.value,
780
+ "hwpxProfile": spec.profile.value,
781
+ "hwpxMutates": spec.mutates,
782
+ "hwpxReplacementTools": list(spec.replacement_tools),
783
+ }
784
+ resolved.append((spec, function, signature))
785
+ schema_entries.append((spec.name, function, description, meta))
786
+ if errors:
787
+ raise RuntimeError("ToolSpec binding failed:\n- " + "\n- ".join(errors))
788
+
789
+ try:
790
+ snapshots = describe_callables(schema_entries)
791
+ except Exception as exc:
792
+ raise RuntimeError(f"ToolSpec schema binding failed: {exc}") from exc
793
+
794
+ bound: list[BoundToolSpec] = []
795
+ for spec, function, signature in resolved:
796
+ parameters = tuple(
797
+ SchemaParameter(
798
+ name=parameter.name,
799
+ annotation=_annotation_name(parameter.annotation),
800
+ required=parameter.default is inspect.Parameter.empty,
801
+ )
802
+ for parameter in signature.parameters.values()
803
+ if parameter.kind
804
+ not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
805
+ )
806
+ bound.append(
807
+ BoundToolSpec(
808
+ spec=spec,
809
+ function=function,
810
+ signature=str(signature),
811
+ parameters=parameters,
812
+ description=snapshots[spec.name].description,
813
+ input_schema=snapshots[spec.name].input_schema,
814
+ output_schema=snapshots[spec.name].output_schema,
815
+ availability=ToolAvailability.AVAILABLE,
816
+ availability_reason=None,
817
+ )
818
+ )
819
+ return RegisteredToolRegistry(tuple(bound))
820
+
821
+
822
+ def _deprecated_wrapper(item: BoundToolSpec) -> Callable[..., Any]:
823
+ replacement_tools = item.spec.replacement_tools
824
+ function = item.function
825
+
826
+ @functools.wraps(function)
827
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
828
+ result = function(*args, **kwargs)
829
+ if isinstance(result, Mapping):
830
+ payload = dict(result)
831
+ payload.setdefault(
832
+ "deprecation",
833
+ {
834
+ "status": "deprecated",
835
+ "tool": item.spec.name,
836
+ "replacementTools": list(replacement_tools),
837
+ "message": "This tool is retained for one transition release.",
838
+ },
839
+ )
840
+ return payload
841
+ return result
842
+
843
+ setattr(wrapped, "__hwpx_original_callable__", function)
844
+ return wrapped
845
+
846
+
847
+ def register_fastmcp_tools(
848
+ mcp: Any,
849
+ namespace: Mapping[str, Any],
850
+ *,
851
+ advanced: bool,
852
+ ) -> RegisteredToolRegistry:
853
+ """Bind the full contract, then register the selected deterministic profile."""
854
+
855
+ global _BOUND_TOOL_REGISTRY
856
+ canonical = bind_tool_specs(namespace, advanced=None)
857
+ _BOUND_TOOL_REGISTRY = canonical
858
+ canonical_by_name = canonical.by_name()
859
+ active = RegisteredToolRegistry(
860
+ tuple(canonical_by_name[spec.name] for spec in active_tool_specs(advanced=advanced))
861
+ )
862
+ for item in active.tools:
863
+ function = (
864
+ _deprecated_wrapper(item)
865
+ if item.spec.classification is ToolClassification.DEPRECATED
866
+ else item.function
867
+ )
868
+ register_canonical_tool(
869
+ mcp,
870
+ name=item.spec.name,
871
+ func=function,
872
+ description=item.description,
873
+ meta={
874
+ "hwpxLifecycle": item.spec.classification.value,
875
+ "hwpxProfile": item.spec.profile.value,
876
+ "hwpxMutates": item.spec.mutates,
877
+ "hwpxReplacementTools": list(item.spec.replacement_tools),
878
+ },
879
+ )
880
+ report = validate_registered_tools(mcp, active)
881
+ if not report["ok"]:
882
+ raise RuntimeError(f"FastMCP registry validation failed: {report!r}")
883
+ return active
884
+
885
+
886
+ def validate_registered_tools(mcp: Any, registry: RegisteredToolRegistry) -> dict[str, Any]:
887
+ """Compare live names, callable identity, description, and both schemas."""
888
+
889
+ actual_by_name = snapshot_runtime_tools(mcp)
890
+ expected_by_name = registry.by_name()
891
+ expected_names = registry.names
892
+ actual_names = tuple(actual_by_name)
893
+ order_mismatch = actual_names != expected_names
894
+ callable_mismatches: list[str] = []
895
+ input_schema_mismatches: list[str] = []
896
+ output_schema_mismatches: list[str] = []
897
+ description_mismatches: list[str] = []
898
+ for item in registry.tools:
899
+ actual = actual_by_name.get(item.spec.name)
900
+ if actual is None:
901
+ continue
902
+ if actual.callable is not item.function:
903
+ callable_mismatches.append(item.spec.name)
904
+ if actual.input_schema != item.input_schema:
905
+ input_schema_mismatches.append(item.spec.name)
906
+ if actual.output_schema != item.output_schema:
907
+ output_schema_mismatches.append(item.spec.name)
908
+ if actual.description != item.description:
909
+ description_mismatches.append(item.spec.name)
910
+ missing = [name for name in expected_names if name not in actual_by_name]
911
+ unexpected = [name for name in actual_names if name not in expected_by_name]
912
+ unavailable = [
913
+ item.spec.name
914
+ for item in registry.tools
915
+ if item.availability is not ToolAvailability.AVAILABLE
916
+ ]
917
+ return {
918
+ "ok": not any(
919
+ (
920
+ missing,
921
+ unexpected,
922
+ callable_mismatches,
923
+ input_schema_mismatches,
924
+ output_schema_mismatches,
925
+ description_mismatches,
926
+ unavailable,
927
+ order_mismatch,
928
+ )
929
+ ),
930
+ "missing": missing,
931
+ "unexpected": unexpected,
932
+ "callableMismatches": callable_mismatches,
933
+ "inputSchemaMismatches": input_schema_mismatches,
934
+ "outputSchemaMismatches": output_schema_mismatches,
935
+ "descriptionMismatches": description_mismatches,
936
+ "unavailable": unavailable,
937
+ "orderMismatch": order_mismatch,
938
+ "expectedOrder": list(expected_names),
939
+ "actualOrder": list(actual_names),
940
+ }
941
+
942
+
943
+ async def validate_fastmcp_tools(mcp: Any, registry: RegisteredToolRegistry) -> dict[str, Any]:
944
+ """Async compatibility wrapper retained for existing diagnostics."""
945
+
946
+ return validate_registered_tools(mcp, registry)
947
+
948
+
949
+ __all__ = [
950
+ "AvailabilityReason",
951
+ "AvailabilityReasonCode",
952
+ "AvailabilityRequirement",
953
+ "BASELINE_DOMAIN_SPECS",
954
+ "BASELINE_TOOL_SPECS",
955
+ "BoundToolSpec",
956
+ "DOMAIN_SPECS",
957
+ "MIN_AUTOMATION_VERSION",
958
+ "MIN_MCP_VERSION",
959
+ "MIN_PYTHON_HWPX",
960
+ "MIN_SKILL_VERSION",
961
+ "RegisteredToolRegistry",
962
+ "RELEASED_CONTRACT_HASH",
963
+ "SchemaBinding",
964
+ "TOOL_SPECS",
965
+ "ToolAvailability",
966
+ "ToolClassification",
967
+ "ToolProfile",
968
+ "ToolSpec",
969
+ "active_tool_specs",
970
+ "bind_tool_specs",
971
+ "bound_tool_registry",
972
+ "classification_counts",
973
+ "classification_payload",
974
+ "contract_hash",
975
+ "contract_payload",
976
+ "expected_tool_names",
977
+ "expected_tool_order",
978
+ "register_fastmcp_tools",
979
+ "skill_required_tool_names",
980
+ "validate_fastmcp_tools",
981
+ "validate_registered_tools",
982
+ ]