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,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
|
+
]
|