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,622 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Atomic typed blueprint replay orchestration."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import tempfile
10
+ from collections.abc import Callable, Mapping, MutableMapping
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from hwpx.document import HwpxDocument
15
+ from hwpx.quality import SavePipeline
16
+ from hwpx.tools.package_validator import validate_editor_open_safety
17
+
18
+ from ..catalog import catalog_hash
19
+ from ..commands import _error_from_exception, _member_diff, _quality_policy, _record_for_native, _revision
20
+ from ..document import HwpxAgentDocument
21
+ from ..model import AgentContractError
22
+ from .bundle import BlueprintBundle, read_blueprint_bundle
23
+ from .dump import dump_document_blueprint
24
+ from .mapping import ReplayPlan, materialize_dependencies, plan_replay
25
+ from .model import BlueprintReplayResult, validate_replay_request
26
+ from .native import CreatedBinding, TypedNativeBridge
27
+
28
+ IdempotencyStore = MutableMapping[str, Any]
29
+ FaultInjector = Callable[[str, int | None], None]
30
+ DomainVerifier = Callable[[bytes, Mapping[str, Any]], Mapping[str, Any]]
31
+ _EMPTY_REVISION = "sha256:" + hashlib.sha256(b"").hexdigest()
32
+
33
+
34
+ def _call(injector: FaultInjector | None, stage: str, index: int | None = None) -> None:
35
+ if injector is not None:
36
+ injector(stage, index)
37
+
38
+
39
+ def _request_hash(request: Mapping[str, Any]) -> str:
40
+ payload = json.dumps(request, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
41
+ return _revision(payload)
42
+
43
+
44
+ def _output_fingerprint(path: Path) -> dict[str, Any]:
45
+ if not path.exists():
46
+ return {"exists": False, "sha256": None}
47
+ if not path.is_file():
48
+ return {"exists": True, "sha256": None}
49
+ return {"exists": True, "sha256": _revision(path.read_bytes())}
50
+
51
+
52
+ def _node_map(view: HwpxAgentDocument, bindings: Mapping[str, CreatedBinding]) -> dict[str, str]:
53
+ result: dict[str, str] = {}
54
+ for blueprint_id, binding in bindings.items():
55
+ if binding.kind == "form-field":
56
+ record = next(
57
+ (
58
+ candidate
59
+ for candidate in view.records
60
+ if candidate.kind == "form-field" and candidate.attributes.get("id") == binding.native_id
61
+ ),
62
+ None,
63
+ )
64
+ if record is None:
65
+ raise AgentContractError("not_found", "replayed form field was not projected", target=blueprint_id)
66
+ else:
67
+ record = _record_for_native(view, binding.native)
68
+ result[blueprint_id] = record.path
69
+ return result
70
+
71
+
72
+ def _semantic_nodes(
73
+ manifest: Mapping[str, Any], dependency_targets: Mapping[str, Mapping[str, Any]] | None = None
74
+ ) -> list[dict[str, Any]]:
75
+ normalized: list[dict[str, Any]] = []
76
+ for node in manifest["nodes"]:
77
+ properties = dict(node["properties"])
78
+ if dependency_targets and node["kind"] == "paragraph" and node["styleRefs"]:
79
+ mapped = dependency_targets.get(str(node["styleRefs"][0]))
80
+ identity = mapped.get("identity") if mapped else None
81
+ if isinstance(identity, Mapping) and identity.get("name"):
82
+ properties["style"] = identity["name"]
83
+ normalized.append(
84
+ {
85
+ "blueprintId": str(node["blueprintId"]),
86
+ "kind": str(node["kind"]),
87
+ "properties": properties,
88
+ "children": list(node["children"]),
89
+ "references": list(node["references"]),
90
+ }
91
+ )
92
+ return normalized
93
+
94
+
95
+ def _semantic_equivalence(
96
+ source: Mapping[str, Any],
97
+ candidate: Mapping[str, Any],
98
+ dependency_targets: Mapping[str, Mapping[str, Any]],
99
+ ) -> dict[str, Any]:
100
+ source_nodes = _semantic_nodes(source, dependency_targets)
101
+ candidate_nodes = _semantic_nodes(candidate)
102
+ equivalent = source_nodes == candidate_nodes
103
+ return {
104
+ "ok": equivalent,
105
+ "sourceNodeCount": len(source_nodes),
106
+ "candidateNodeCount": len(candidate_nodes),
107
+ "kindSequenceMatched": [item["kind"] for item in source_nodes]
108
+ == [item["kind"] for item in candidate_nodes],
109
+ "propertiesMatched": equivalent,
110
+ }
111
+
112
+
113
+ def _dependency_receipt(targets: Mapping[str, Mapping[str, Any]]) -> dict[str, Any]:
114
+ return {
115
+ key: {
116
+ "family": item["family"],
117
+ "action": item["action"],
118
+ "fidelity": item["fidelity"],
119
+ "identity": dict(item["identity"]),
120
+ }
121
+ for key, item in sorted(targets.items())
122
+ }
123
+
124
+
125
+ def _fidelity_receipt(plan: Any, targets: Mapping[str, Mapping[str, Any]]) -> dict[str, Any]:
126
+ dependencies = {
127
+ key: str(item["fidelity"]) for key, item in sorted(targets.items())
128
+ }
129
+ nodes = {
130
+ str(item["blueprintId"]): str(item["fidelity"]) for item in plan.node_fidelity
131
+ }
132
+ levels = set(dependencies.values()) | set(nodes.values())
133
+ strict_ok = levels <= {"exact", "mapped"}
134
+ return {
135
+ "strict": True,
136
+ "ok": strict_ok,
137
+ "ceiling": "exact" if levels <= {"exact"} else "mapped",
138
+ "nodes": nodes,
139
+ "dependencies": dependencies,
140
+ "degraded": [],
141
+ "unsupported": [],
142
+ }
143
+
144
+
145
+ def _replayed_result(prior: BlueprintReplayResult) -> BlueprintReplayResult:
146
+ verification = dict(prior.verification_report)
147
+ idempotency = dict(verification.get("idempotency", {}))
148
+ idempotency["replayed"] = True
149
+ verification["idempotency"] = idempotency
150
+ return BlueprintReplayResult(
151
+ ok=prior.ok,
152
+ rolled_back=prior.rolled_back,
153
+ dry_run=prior.dry_run,
154
+ input_revision=prior.input_revision,
155
+ document_revision=prior.document_revision,
156
+ output_filename=prior.output_filename,
157
+ blueprint_hash=prior.blueprint_hash,
158
+ root_path=prior.root_path,
159
+ node_map=prior.node_map,
160
+ dependency_maps=prior.dependency_maps,
161
+ fidelity=prior.fidelity,
162
+ semantic_diff=prior.semantic_diff,
163
+ verification_report=verification,
164
+ error=prior.error,
165
+ )
166
+
167
+
168
+ class _ReplayState:
169
+ """Mutable bookkeeping threaded through the replay steps below.
170
+
171
+ Tracked on an object (rather than as bare locals in the entry point) so
172
+ that if any step raises, the top-level exception handler can still see
173
+ whatever was set so far and build an honest rollback/failure result --
174
+ exactly as the prior monolithic try/except body relied on its locals
175
+ already being assigned at the point of failure.
176
+ """
177
+
178
+ __slots__ = (
179
+ "normalized",
180
+ "verification",
181
+ "input_revision",
182
+ "blueprint_hash",
183
+ "output_filename",
184
+ "output_path",
185
+ "output_before",
186
+ "temp_path",
187
+ )
188
+
189
+ def __init__(self) -> None:
190
+ self.normalized: dict[str, Any] | None = None
191
+ self.verification: dict[str, Any] = {}
192
+ self.input_revision = _EMPTY_REVISION
193
+ self.blueprint_hash = "sha256:" + "0" * 64
194
+ self.output_filename = ""
195
+ self.output_path: Path | None = None
196
+ self.output_before: dict[str, Any] | None = None
197
+ self.temp_path: Path | None = None
198
+
199
+
200
+ def replay_document_blueprint(
201
+ request: Mapping[str, Any],
202
+ *,
203
+ idempotency_store: IdempotencyStore | None = None,
204
+ fault_injector: FaultInjector | None = None,
205
+ domain_verifier: DomainVerifier | None = None,
206
+ save_pipeline: SavePipeline | None = None,
207
+ ) -> BlueprintReplayResult:
208
+ """Validate, map, construct, verify, and atomically commit one blueprint."""
209
+
210
+ state = _ReplayState()
211
+ try:
212
+ return _run_replay(
213
+ state,
214
+ request,
215
+ idempotency_store=idempotency_store,
216
+ fault_injector=fault_injector,
217
+ domain_verifier=domain_verifier,
218
+ save_pipeline=save_pipeline,
219
+ )
220
+ except BaseException as exc:
221
+ return _replay_failure_result(state, request, exc)
222
+
223
+
224
+ def _replay_failure_result(
225
+ state: _ReplayState, request: Mapping[str, Any], exc: BaseException
226
+ ) -> BlueprintReplayResult:
227
+ if state.temp_path is not None:
228
+ try:
229
+ state.temp_path.unlink()
230
+ except FileNotFoundError:
231
+ pass
232
+ if state.output_path is not None and state.output_before is not None:
233
+ state.verification["rollback"] = {
234
+ "ok": _output_fingerprint(state.output_path) == state.output_before,
235
+ "outputBefore": state.output_before,
236
+ "outputAfter": _output_fingerprint(state.output_path),
237
+ }
238
+ return BlueprintReplayResult(
239
+ ok=False,
240
+ rolled_back=True,
241
+ dry_run=bool((state.normalized or request).get("dryRun", False)),
242
+ input_revision=state.input_revision,
243
+ document_revision=state.input_revision,
244
+ output_filename=state.output_filename,
245
+ blueprint_hash=state.blueprint_hash,
246
+ verification_report=state.verification,
247
+ error=_error_from_exception(exc),
248
+ )
249
+
250
+
251
+ def _replay_load_bundle(
252
+ state: _ReplayState,
253
+ normalized: Mapping[str, Any],
254
+ fault_injector: FaultInjector | None,
255
+ ) -> tuple[BlueprintBundle, dict[str, Any]]:
256
+ _call(fault_injector, "before_bundle_validation")
257
+ bundle = read_blueprint_bundle(str(normalized["bundle"]["filename"]))
258
+ manifest = dict(bundle.manifest)
259
+ blueprint_hash = str(manifest["blueprintHash"])
260
+ state.blueprint_hash = blueprint_hash
261
+ if blueprint_hash != normalized["bundle"]["blueprintHash"]:
262
+ raise AgentContractError("verification_failed", "request blueprintHash does not match bundle", target="bundle")
263
+ if manifest["catalogHash"] != catalog_hash():
264
+ raise AgentContractError("verification_failed", "blueprint catalog hash does not match runtime", target="catalogHash")
265
+ _call(fault_injector, "after_bundle_validation")
266
+ state.verification["bundle"] = {
267
+ "validatedBeforeTargetAccess": True,
268
+ "blueprintHash": blueprint_hash,
269
+ "bundleSha256": bundle.bundle_sha256,
270
+ "bytes": bundle.size,
271
+ }
272
+ return bundle, manifest
273
+
274
+
275
+ def _replay_idempotency_lookup(
276
+ verification: dict[str, Any],
277
+ idempotency_store: IdempotencyStore | None,
278
+ *,
279
+ key: str | None,
280
+ request_hash: str,
281
+ ) -> BlueprintReplayResult | None:
282
+ verification["idempotency"] = {
283
+ "keyProvided": key is not None,
284
+ "requestHash": request_hash,
285
+ "replayed": False,
286
+ "store": "caller-owned" if idempotency_store is not None else "none",
287
+ }
288
+ if key is not None and idempotency_store is not None and key in idempotency_store:
289
+ cached = idempotency_store[key]
290
+ if not isinstance(cached, Mapping) or cached.get("requestHash") != request_hash:
291
+ raise AgentContractError("idempotency_conflict", "idempotency key was used for a different replay")
292
+ prior = cached.get("result")
293
+ if not isinstance(prior, BlueprintReplayResult):
294
+ raise AgentContractError("invariant_violation", "idempotency store contains an invalid replay result")
295
+ return _replayed_result(prior)
296
+ return None
297
+
298
+
299
+ def _replay_load_target(
300
+ normalized: Mapping[str, Any],
301
+ state: _ReplayState,
302
+ fault_injector: FaultInjector | None,
303
+ ) -> tuple[Path, bytes, Path]:
304
+ _call(fault_injector, "before_target_read")
305
+ input_path = Path(str(normalized["target"]["input"]))
306
+ output_path = Path(state.output_filename)
307
+ state.output_path = output_path
308
+ input_data = input_path.read_bytes()
309
+ output_before = _output_fingerprint(output_path)
310
+ state.output_before = output_before
311
+ if output_before["exists"] and not normalized["target"]["overwrite"] and not normalized["dryRun"]:
312
+ raise AgentContractError("identity_collision", "replay output already exists", target="target.output")
313
+ if not output_path.parent.exists():
314
+ raise AgentContractError("not_found", "replay output parent does not exist", target="target.output")
315
+ input_revision = _revision(input_data)
316
+ state.input_revision = input_revision
317
+ _call(fault_injector, "after_target_read")
318
+ expected_revision = normalized["expectedRevision"]
319
+ state.verification["revision"] = {
320
+ "expected": expected_revision,
321
+ "actual": input_revision,
322
+ "matched": expected_revision in {None, input_revision},
323
+ }
324
+ if expected_revision not in {None, input_revision}:
325
+ raise AgentContractError("stale_revision", "expectedRevision does not match target input")
326
+ input_safety = validate_editor_open_safety(input_data)
327
+ state.verification["inputOpenSafety"] = input_safety.to_dict()
328
+ if not input_safety.ok:
329
+ raise AgentContractError("verification_failed", "target input failed editor-open safety")
330
+ _call(fault_injector, "after_target_validation")
331
+ return input_path, input_data, output_path
332
+
333
+
334
+ def _replay_materialize_dependencies(
335
+ document: HwpxDocument,
336
+ plan: ReplayPlan,
337
+ bundle: BlueprintBundle,
338
+ normalized: Mapping[str, Any],
339
+ fault_injector: FaultInjector | None,
340
+ ) -> tuple[dict[str, dict[str, Any]], dict[str, Any], dict[str, Any]]:
341
+ _call(fault_injector, "before_dependencies")
342
+ dependency_targets = materialize_dependencies(document, plan, dict(bundle.assets))
343
+ _call(fault_injector, "after_dependencies")
344
+ dependency_maps = _dependency_receipt(dependency_targets)
345
+ fidelity = _fidelity_receipt(plan, dependency_targets)
346
+ if normalized["mappingPolicy"]["strict"] and not fidelity["ok"]:
347
+ raise AgentContractError("unsupported_content", "strict replay fidelity failed")
348
+ return dependency_targets, dependency_maps, fidelity
349
+
350
+
351
+ def _replay_build_and_verify_candidate(
352
+ document: HwpxDocument,
353
+ input_data: bytes,
354
+ node_map: Mapping[str, str],
355
+ root_path: str,
356
+ plan: ReplayPlan,
357
+ manifest: Mapping[str, Any],
358
+ dependency_targets: Mapping[str, Mapping[str, Any]],
359
+ normalized: Mapping[str, Any],
360
+ fault_injector: FaultInjector | None,
361
+ ) -> tuple[bytes, str, dict[str, Any], dict[str, Any], dict[str, Any]]:
362
+ _call(fault_injector, "before_serialize")
363
+ candidate_data = document.to_bytes()
364
+ candidate_revision = _revision(candidate_data)
365
+ _call(fault_injector, "after_serialize")
366
+ candidate_safety = validate_editor_open_safety(candidate_data)
367
+ safety_dict = candidate_safety.to_dict()
368
+ byte_report = _member_diff(input_data, candidate_data)
369
+ if not candidate_safety.ok or not byte_report.get("ok"):
370
+ raise AgentContractError("verification_failed", "candidate failed structural verification")
371
+ with HwpxAgentDocument.open(candidate_data) as reopened:
372
+ for blueprint_id, target_path in node_map.items():
373
+ reopened.resolve_record(target_path)
374
+ if plan.root_kind == "document":
375
+ semantic_diff = {
376
+ "ok": True,
377
+ "scope": "document-root-children",
378
+ "sourceNodeCount": len(manifest["nodes"]),
379
+ "candidateNodeCount": len(node_map),
380
+ "kindSequenceMatched": True,
381
+ "propertiesMatched": None,
382
+ }
383
+ else:
384
+ candidate_dump = dump_document_blueprint(
385
+ candidate_data,
386
+ path=root_path,
387
+ mode=str(normalized["mode"]),
388
+ require_replayable=True,
389
+ )
390
+ semantic_diff = _semantic_equivalence(manifest, candidate_dump.manifest, dependency_targets)
391
+ if not semantic_diff["ok"]:
392
+ raise AgentContractError("verification_failed", "replayed semantic graph differs from blueprint")
393
+ return candidate_data, candidate_revision, semantic_diff, safety_dict, byte_report
394
+
395
+
396
+ def _replay_domain_verification(
397
+ candidate_data: bytes,
398
+ normalized: Mapping[str, Any],
399
+ requirements: set[str],
400
+ domain_verifier: DomainVerifier | None,
401
+ verification: dict[str, Any],
402
+ ) -> None:
403
+ if "domain" in requirements:
404
+ if domain_verifier is None:
405
+ raise AgentContractError("verification_failed", "required domain verifier is unavailable", target="domain")
406
+ domain_report = dict(domain_verifier(candidate_data, normalized))
407
+ json.dumps(domain_report, ensure_ascii=False, sort_keys=True)
408
+ verification["domain"] = domain_report
409
+ if not domain_report.get("ok"):
410
+ raise AgentContractError("verification_failed", "domain verification failed", target="domain")
411
+ else:
412
+ verification["domain"] = {"ok": None, "status": "not-requested"}
413
+
414
+
415
+ def _replay_run_save_pipeline(
416
+ state: _ReplayState,
417
+ candidate_data: bytes,
418
+ input_path: Path,
419
+ output_path: Path,
420
+ document: HwpxDocument,
421
+ normalized: Mapping[str, Any],
422
+ requirements: set[str],
423
+ save_pipeline: SavePipeline | None,
424
+ ) -> Any:
425
+ pipeline = save_pipeline or SavePipeline()
426
+ quality = _quality_policy(normalized["quality"]).with_(require_reference_integrity=True)
427
+ if "realHancom" in requirements:
428
+ quality = quality.with_(require_visual_complete=True, render_check="required")
429
+ if normalized["dryRun"]:
430
+ return pipeline.run(
431
+ candidate_data,
432
+ output_path=None,
433
+ quality=quality,
434
+ before=input_path,
435
+ reference_document=document,
436
+ publish="never",
437
+ source_label="agent.replay_document_blueprint",
438
+ )
439
+ descriptor, temp_name = tempfile.mkstemp(
440
+ prefix=f".{output_path.name}.", suffix=".hwpx", dir=output_path.parent
441
+ )
442
+ os.close(descriptor)
443
+ os.unlink(temp_name)
444
+ state.temp_path = Path(temp_name)
445
+ return pipeline.run(
446
+ candidate_data,
447
+ output_path=state.temp_path,
448
+ quality=quality,
449
+ before=input_path,
450
+ reference_document=document,
451
+ publish="on_pass",
452
+ source_label="agent.replay_document_blueprint",
453
+ )
454
+
455
+
456
+ def _replay_commit(
457
+ state: _ReplayState,
458
+ output_path: Path,
459
+ normalized: Mapping[str, Any],
460
+ fault_injector: FaultInjector | None,
461
+ ) -> dict[str, Any]:
462
+ temp_path = state.temp_path
463
+ if temp_path is None or not temp_path.exists():
464
+ raise AgentContractError("verification_failed", "SavePipeline did not materialize candidate")
465
+ _call(fault_injector, "before_commit")
466
+ if normalized["target"]["overwrite"]:
467
+ os.replace(temp_path, output_path)
468
+ else:
469
+ try:
470
+ os.link(temp_path, output_path)
471
+ except FileExistsError as exc:
472
+ raise AgentContractError("identity_collision", "replay output appeared before commit") from exc
473
+ os.unlink(temp_path)
474
+ state.temp_path = None
475
+ return {"ok": True, "atomic": True, "savedOnce": True}
476
+
477
+
478
+ def _run_replay(
479
+ state: _ReplayState,
480
+ request: Mapping[str, Any],
481
+ *,
482
+ idempotency_store: IdempotencyStore | None,
483
+ fault_injector: FaultInjector | None,
484
+ domain_verifier: DomainVerifier | None,
485
+ save_pipeline: SavePipeline | None,
486
+ ) -> BlueprintReplayResult:
487
+ normalized = validate_replay_request(request)
488
+ state.normalized = normalized
489
+ state.output_filename = str(normalized["target"]["output"])
490
+
491
+ bundle, manifest = _replay_load_bundle(state, normalized, fault_injector)
492
+
493
+ request_hash = _request_hash(normalized)
494
+ key = normalized["idempotencyKey"]
495
+ cached_result = _replay_idempotency_lookup(
496
+ state.verification, idempotency_store, key=key, request_hash=request_hash
497
+ )
498
+ if cached_result is not None:
499
+ return cached_result
500
+
501
+ input_path, input_data, output_path = _replay_load_target(normalized, state, fault_injector)
502
+
503
+ with HwpxDocument.open(input_data) as document:
504
+ view = HwpxAgentDocument.from_document(document, revision=state.input_revision)
505
+ parent = view.resolve_record(
506
+ str(normalized["targetParent"]),
507
+ expected_revision=state.input_revision,
508
+ require_stable=normalized["expectedRevision"] is None,
509
+ )
510
+ _call(fault_injector, "before_plan")
511
+ plan = plan_replay(
512
+ document,
513
+ view,
514
+ manifest,
515
+ parent,
516
+ mode=str(normalized["mode"]),
517
+ )
518
+ state.verification["plan"] = plan.to_dict()
519
+ _call(fault_injector, "after_plan")
520
+
521
+ dependency_targets, dependency_maps, fidelity = _replay_materialize_dependencies(
522
+ document, plan, bundle, normalized, fault_injector
523
+ )
524
+
525
+ _call(fault_injector, "before_nodes")
526
+ bridge = TypedNativeBridge(document, manifest, dependency_targets)
527
+ bindings = bridge.create_root(parent, dict(normalized["position"]), view)
528
+ _call(fault_injector, "after_nodes")
529
+ fresh_view = HwpxAgentDocument.from_document(document, revision=state.input_revision)
530
+ node_map = _node_map(fresh_view, bindings)
531
+ root_path = node_map[plan.root_id]
532
+
533
+ candidate_data, candidate_revision, semantic_diff, safety_dict, byte_report = (
534
+ _replay_build_and_verify_candidate(
535
+ document,
536
+ input_data,
537
+ node_map,
538
+ root_path,
539
+ plan,
540
+ manifest,
541
+ dependency_targets,
542
+ normalized,
543
+ fault_injector,
544
+ )
545
+ )
546
+ state.verification.update(
547
+ {
548
+ "candidateRevision": candidate_revision,
549
+ "package": safety_dict["validatePackage"],
550
+ "reopen": safety_dict["reopen"],
551
+ "openSafety": safety_dict,
552
+ "bytePreservation": byte_report,
553
+ "semanticDiff": semantic_diff,
554
+ "dependencyMaps": dependency_maps,
555
+ "nodeMap": node_map,
556
+ "fidelity": fidelity,
557
+ }
558
+ )
559
+ _call(fault_injector, "after_candidate_verification")
560
+
561
+ requirements = set(normalized["verificationRequirements"])
562
+ _replay_domain_verification(candidate_data, normalized, requirements, domain_verifier, state.verification)
563
+
564
+ _call(fault_injector, "before_save_pipeline")
565
+ quality_report = _replay_run_save_pipeline(
566
+ state,
567
+ candidate_data,
568
+ input_path,
569
+ output_path,
570
+ document,
571
+ normalized,
572
+ requirements,
573
+ save_pipeline,
574
+ )
575
+ _call(fault_injector, "after_save_pipeline")
576
+ save_report = quality_report.to_dict()
577
+ save_report["outputPath"] = None if normalized["dryRun"] else state.output_filename
578
+ save_report["debugPath"] = None
579
+ state.verification["savePipeline"] = save_report
580
+ state.verification["realHancom"] = {
581
+ "required": "realHancom" in requirements,
582
+ "ok": quality_report.visual_complete,
583
+ "status": quality_report.visual_complete_status,
584
+ "renderChecked": quality_report.render_checked,
585
+ }
586
+ if not quality_report.ok:
587
+ raise AgentContractError("verification_failed", "SavePipeline rejected replay candidate")
588
+ if "realHancom" in requirements and not quality_report.visual_complete:
589
+ raise AgentContractError("verification_failed", "required real-Hancom verification is unavailable")
590
+
591
+ if not normalized["dryRun"]:
592
+ commit_record = _replay_commit(state, output_path, normalized, fault_injector)
593
+ state.verification["commit"] = commit_record
594
+ else:
595
+ state.verification["commit"] = {"ok": True, "atomic": True, "savedOnce": False, "dryRun": True}
596
+
597
+ result = BlueprintReplayResult(
598
+ ok=True,
599
+ rolled_back=False,
600
+ dry_run=bool(normalized["dryRun"]),
601
+ input_revision=state.input_revision,
602
+ document_revision=candidate_revision,
603
+ output_filename=state.output_filename,
604
+ blueprint_hash=state.blueprint_hash,
605
+ root_path=root_path,
606
+ node_map=node_map,
607
+ dependency_maps=dependency_maps,
608
+ fidelity=fidelity,
609
+ semantic_diff=semantic_diff,
610
+ verification_report=state.verification,
611
+ )
612
+ if key is not None and idempotency_store is not None:
613
+ idempotency_store[key] = {"requestHash": request_hash, "result": result}
614
+ return result
615
+
616
+
617
+ __all__ = [
618
+ "DomainVerifier",
619
+ "FaultInjector",
620
+ "IdempotencyStore",
621
+ "replay_document_blueprint",
622
+ ]