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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- 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,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
|
+
]
|