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,520 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Typed, deterministic document/subtree blueprint projection."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ..catalog import catalog_hash
|
|
15
|
+
from ..document import HwpxAgentDocument, NodeRecord
|
|
16
|
+
from ..model import AGENT_CATALOG_SCHEMA, NODE_KINDS, AgentContractError
|
|
17
|
+
from .bundle import (
|
|
18
|
+
ALLOWED_MEDIA_TYPES,
|
|
19
|
+
build_blueprint_bundle,
|
|
20
|
+
read_blueprint_bundle,
|
|
21
|
+
write_blueprint_bundle,
|
|
22
|
+
)
|
|
23
|
+
from .model import (
|
|
24
|
+
BLUEPRINT_MODES,
|
|
25
|
+
BLUEPRINT_SCHEMA,
|
|
26
|
+
MAX_BLUEPRINT_DEPTH,
|
|
27
|
+
MAX_BLUEPRINT_NODES,
|
|
28
|
+
blueprint_limits,
|
|
29
|
+
validate_blueprint_manifest,
|
|
30
|
+
with_blueprint_hash,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class BlueprintDumpResult:
|
|
36
|
+
"""One dump receipt plus the validated in-memory artifact."""
|
|
37
|
+
|
|
38
|
+
manifest: Mapping[str, Any]
|
|
39
|
+
assets: Mapping[str, bytes]
|
|
40
|
+
bundle_bytes: bytes
|
|
41
|
+
bundle_sha256: str
|
|
42
|
+
output_filename: str | None
|
|
43
|
+
|
|
44
|
+
def to_dict(self, *, include_manifest: bool = True) -> dict[str, Any]:
|
|
45
|
+
result: dict[str, Any] = {
|
|
46
|
+
"ok": True,
|
|
47
|
+
"schemaVersion": self.manifest["schemaVersion"],
|
|
48
|
+
"blueprintHash": self.manifest["blueprintHash"],
|
|
49
|
+
"bundleSha256": self.bundle_sha256,
|
|
50
|
+
"bundleBytes": len(self.bundle_bytes),
|
|
51
|
+
"outputFilename": self.output_filename,
|
|
52
|
+
"mode": self.manifest["mode"],
|
|
53
|
+
"root": dict(self.manifest["root"]),
|
|
54
|
+
"nodeCount": len(self.manifest["nodes"]),
|
|
55
|
+
"dependencyCount": (
|
|
56
|
+
len(self.manifest["styles"])
|
|
57
|
+
+ len(self.manifest["numbering"])
|
|
58
|
+
+ len(self.manifest["resources"])
|
|
59
|
+
),
|
|
60
|
+
"assetCount": len(self.assets),
|
|
61
|
+
"unsupported": list(self.manifest["unsupported"]),
|
|
62
|
+
"fidelity": dict(self.manifest["fidelity"]),
|
|
63
|
+
}
|
|
64
|
+
if include_manifest:
|
|
65
|
+
result["manifest"] = dict(self.manifest)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _local_name(element: Any) -> str:
|
|
70
|
+
return str(getattr(element, "tag", "")).rsplit("}", 1)[-1]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _canonical_digest(value: Mapping[str, Any]) -> str:
|
|
74
|
+
encoded = json.dumps(
|
|
75
|
+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
76
|
+
).encode("utf-8")
|
|
77
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _dependency(kind: str, properties: Mapping[str, Any], *, name: str | None = None) -> dict[str, Any]:
|
|
81
|
+
detached = dict(properties)
|
|
82
|
+
digest = _canonical_digest(detached)
|
|
83
|
+
result: dict[str, Any] = {
|
|
84
|
+
"key": f"{kind}:{digest}",
|
|
85
|
+
"kind": kind,
|
|
86
|
+
"signature": f"sha256:{digest}",
|
|
87
|
+
"properties": detached,
|
|
88
|
+
}
|
|
89
|
+
if name:
|
|
90
|
+
result["name"] = str(name)[:512]
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _semantic_generic(value: Any) -> dict[str, Any]:
|
|
95
|
+
"""Convert a parsed definition to typed semantics without tags or native IDs."""
|
|
96
|
+
|
|
97
|
+
attributes = {
|
|
98
|
+
str(key): str(item)
|
|
99
|
+
for key, item in sorted(getattr(value, "attributes", {}).items())
|
|
100
|
+
if "id" not in str(key).casefold() and "ref" not in str(key).casefold()
|
|
101
|
+
}
|
|
102
|
+
result: dict[str, Any] = {"kind": str(getattr(value, "name", "definition"))}
|
|
103
|
+
if attributes:
|
|
104
|
+
result["properties"] = attributes
|
|
105
|
+
text = getattr(value, "text", None)
|
|
106
|
+
if text:
|
|
107
|
+
result["text"] = str(text)[:16_384]
|
|
108
|
+
children = [_semantic_generic(child) for child in getattr(value, "children", [])]
|
|
109
|
+
if children:
|
|
110
|
+
result["children"] = children
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _numbering_properties(agent: HwpxAgentDocument, record: NodeRecord) -> dict[str, Any] | None:
|
|
115
|
+
if record.kind != "paragraph":
|
|
116
|
+
return None
|
|
117
|
+
paragraph = record.native
|
|
118
|
+
para_pr = agent.document.paragraph_property(paragraph.element.get("paraPrIDRef"))
|
|
119
|
+
heading = para_pr.heading if para_pr is not None else None
|
|
120
|
+
if heading is None or not heading.type or str(heading.type).upper() == "NONE":
|
|
121
|
+
return None
|
|
122
|
+
properties: dict[str, Any] = {
|
|
123
|
+
"type": str(heading.type),
|
|
124
|
+
"level": heading.level,
|
|
125
|
+
}
|
|
126
|
+
ref = heading.id_ref
|
|
127
|
+
if ref is not None and agent.document._root.headers: # type: ignore[attr-defined]
|
|
128
|
+
model = agent.document._root.headers[0].to_model() # type: ignore[attr-defined]
|
|
129
|
+
ref_list = model.ref_list
|
|
130
|
+
definitions = []
|
|
131
|
+
if str(heading.type).upper() == "BULLET" and ref_list and ref_list.bullets:
|
|
132
|
+
definitions = list(ref_list.bullets.bullets)
|
|
133
|
+
elif ref_list and ref_list.numberings:
|
|
134
|
+
definitions = list(ref_list.numberings.numberings)
|
|
135
|
+
for definition in definitions:
|
|
136
|
+
raw_id = getattr(definition, "attributes", {}).get("id")
|
|
137
|
+
if raw_id is not None and str(raw_id) == str(ref):
|
|
138
|
+
properties["definition"] = _semantic_generic(definition)
|
|
139
|
+
break
|
|
140
|
+
return properties
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _style_dependency(agent: HwpxAgentDocument, record: NodeRecord) -> dict[str, Any] | None:
|
|
144
|
+
if record.kind == "paragraph":
|
|
145
|
+
paragraph = record.native
|
|
146
|
+
style = agent.document.style(paragraph.element.get("styleIDRef"))
|
|
147
|
+
properties = {
|
|
148
|
+
"category": "paragraph",
|
|
149
|
+
"name": record.summary.get("style"),
|
|
150
|
+
"type": getattr(style, "type", None),
|
|
151
|
+
"englishName": getattr(style, "eng_name", None),
|
|
152
|
+
"lockForm": getattr(style, "lock_form", None),
|
|
153
|
+
"alignment": record.summary.get("alignment"),
|
|
154
|
+
"breakBefore": record.summary.get("breakBefore"),
|
|
155
|
+
"keepWithNext": record.summary.get("keepWithNext"),
|
|
156
|
+
"lineSpacingPercent": record.summary.get("lineSpacingPercent"),
|
|
157
|
+
}
|
|
158
|
+
return _dependency("style", properties, name=str(record.summary.get("style") or "paragraph"))
|
|
159
|
+
if record.kind == "run":
|
|
160
|
+
properties = {"category": "character"}
|
|
161
|
+
properties.update({key: value for key, value in record.summary.items() if key != "text"})
|
|
162
|
+
return _dependency("char", properties, name="character-format")
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _manifest_image_item(agent: HwpxAgentDocument, item_id: str) -> tuple[str, str] | None:
|
|
167
|
+
matches: list[tuple[str, str]] = []
|
|
168
|
+
for item in agent.document._package._manifest_items(): # type: ignore[attr-defined]
|
|
169
|
+
if str(item.get("id") or "") == item_id:
|
|
170
|
+
matches.append((str(item.get("href") or ""), str(item.get("media-type") or "")))
|
|
171
|
+
if len(matches) > 1:
|
|
172
|
+
raise AgentContractError(
|
|
173
|
+
"invariant_violation", "picture resource identity is duplicated", target=item_id
|
|
174
|
+
)
|
|
175
|
+
return matches[0] if matches else None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _picture_resource(
|
|
179
|
+
agent: HwpxAgentDocument, record: NodeRecord
|
|
180
|
+
) -> tuple[dict[str, Any], bytes] | None:
|
|
181
|
+
if record.kind != "picture":
|
|
182
|
+
return None
|
|
183
|
+
image = next((child for child in record.native.iter() if _local_name(child) == "img"), None)
|
|
184
|
+
item_id = str(image.get("binaryItemIDRef") or "") if image is not None else ""
|
|
185
|
+
if not item_id:
|
|
186
|
+
return None
|
|
187
|
+
manifest_item = _manifest_image_item(agent, item_id)
|
|
188
|
+
if manifest_item is None:
|
|
189
|
+
return None
|
|
190
|
+
package_name, media_type = manifest_item
|
|
191
|
+
media_type = media_type.casefold()
|
|
192
|
+
if media_type not in ALLOWED_MEDIA_TYPES:
|
|
193
|
+
return None
|
|
194
|
+
suffix = Path(package_name).suffix.casefold().lstrip(".")
|
|
195
|
+
if suffix not in ALLOWED_MEDIA_TYPES[media_type]:
|
|
196
|
+
return None
|
|
197
|
+
payload = agent.document._package.read(package_name) # type: ignore[attr-defined]
|
|
198
|
+
digest = hashlib.sha256(payload).hexdigest()
|
|
199
|
+
resource = {
|
|
200
|
+
"key": f"resource:{digest}",
|
|
201
|
+
"sha256": f"sha256:{digest}",
|
|
202
|
+
"mediaType": media_type,
|
|
203
|
+
"size": len(payload),
|
|
204
|
+
"assetPath": f"assets/{digest}.{suffix}",
|
|
205
|
+
"name": str(record.summary.get("name") or "image")[:512],
|
|
206
|
+
}
|
|
207
|
+
return resource, payload
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _first_descendant(element: Any, kind: str) -> Any | None:
|
|
211
|
+
return next((child for child in element.iter() if _local_name(child) == kind), None)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _portable_properties(record: NodeRecord) -> dict[str, Any]:
|
|
215
|
+
properties = dict(record.summary)
|
|
216
|
+
if record.kind == "section":
|
|
217
|
+
# The section part filename is an implementation/package coordinate,
|
|
218
|
+
# and its one-based index is a source-document coordinate. Neither is
|
|
219
|
+
# a portable construction property: replay may append the section at a
|
|
220
|
+
# different target index.
|
|
221
|
+
properties.pop("partId", None)
|
|
222
|
+
properties.pop("index", None)
|
|
223
|
+
elif record.kind == "cell":
|
|
224
|
+
# Row/column are source-document addresses. The graph order and spans
|
|
225
|
+
# carry the portable structure, while replay may insert the cell's row
|
|
226
|
+
# at a different target address.
|
|
227
|
+
properties.pop("row", None)
|
|
228
|
+
properties.pop("column", None)
|
|
229
|
+
elif record.kind == "row":
|
|
230
|
+
# Like cell addresses, the row index is determined by the target
|
|
231
|
+
# insertion point and is not part of the portable row payload.
|
|
232
|
+
properties.pop("index", None)
|
|
233
|
+
elif record.kind == "picture":
|
|
234
|
+
pos = _first_descendant(record.native, "pos")
|
|
235
|
+
if pos is not None:
|
|
236
|
+
properties["treatAsChar"] = str(pos.get("treatAsChar", "1")) not in {"0", "false", "False"}
|
|
237
|
+
properties["alignment"] = pos.get("horzAlign")
|
|
238
|
+
elif record.kind == "shape":
|
|
239
|
+
element = record.native.element
|
|
240
|
+
pos = _first_descendant(element, "pos")
|
|
241
|
+
line = _first_descendant(element, "lineShape")
|
|
242
|
+
fill = _first_descendant(element, "winBrush")
|
|
243
|
+
properties.update(
|
|
244
|
+
{
|
|
245
|
+
"treatAsChar": (
|
|
246
|
+
str(pos.get("treatAsChar", "1")) not in {"0", "false", "False"}
|
|
247
|
+
if pos is not None
|
|
248
|
+
else True
|
|
249
|
+
),
|
|
250
|
+
"lineColor": line.get("color") if line is not None else None,
|
|
251
|
+
"lineWidth": line.get("width") if line is not None else None,
|
|
252
|
+
"fillColor": fill.get("faceColor") if fill is not None else None,
|
|
253
|
+
"ratio": element.get("ratio") if _local_name(element) == "rect" else None,
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
elif record.kind == "form-field":
|
|
257
|
+
properties["hasEnd"] = bool(record.native.get("has_end"))
|
|
258
|
+
return properties
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _native_element(record: NodeRecord) -> Any | None:
|
|
262
|
+
native = record.native
|
|
263
|
+
if record.kind == "form-field":
|
|
264
|
+
return None
|
|
265
|
+
return getattr(native, "element", native)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _host_run_path(record: NodeRecord, selected: Mapping[str, NodeRecord]) -> str | None:
|
|
269
|
+
if record.kind not in {"table", "picture", "shape", "footnote", "endnote"}:
|
|
270
|
+
return None
|
|
271
|
+
element = _native_element(record)
|
|
272
|
+
parent = element.getparent() if element is not None and hasattr(element, "getparent") else None
|
|
273
|
+
for candidate in selected.values():
|
|
274
|
+
if candidate.kind != "run" or candidate.parent_path != record.parent_path:
|
|
275
|
+
continue
|
|
276
|
+
run_element = candidate.native.element
|
|
277
|
+
if parent is run_element or (element is not None and any(child is element for child in run_element)):
|
|
278
|
+
return candidate.path
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _ordered_subtree(
|
|
283
|
+
agent: HwpxAgentDocument, root: NodeRecord
|
|
284
|
+
) -> list[tuple[NodeRecord, int]]:
|
|
285
|
+
by_path = {record.path: record for record in agent.records}
|
|
286
|
+
ordered: list[tuple[NodeRecord, int]] = []
|
|
287
|
+
visiting: set[str] = set()
|
|
288
|
+
visited: set[str] = set()
|
|
289
|
+
|
|
290
|
+
def visit(record: NodeRecord, depth: int) -> None:
|
|
291
|
+
if depth > MAX_BLUEPRINT_DEPTH:
|
|
292
|
+
raise AgentContractError("resource_limit", "blueprint subtree exceeds depth limit", target=record.path)
|
|
293
|
+
if record.path in visiting:
|
|
294
|
+
raise AgentContractError("invariant_violation", "semantic projection contains a cycle", target=record.path)
|
|
295
|
+
if record.path in visited:
|
|
296
|
+
raise AgentContractError(
|
|
297
|
+
"invariant_violation", "semantic projection contains multiple parents", target=record.path
|
|
298
|
+
)
|
|
299
|
+
visiting.add(record.path)
|
|
300
|
+
visited.add(record.path)
|
|
301
|
+
ordered.append((record, depth))
|
|
302
|
+
if len(ordered) > MAX_BLUEPRINT_NODES:
|
|
303
|
+
raise AgentContractError("resource_limit", "blueprint subtree exceeds node limit", target=record.path)
|
|
304
|
+
for child_path in record.child_paths:
|
|
305
|
+
child = by_path.get(child_path)
|
|
306
|
+
if child is None:
|
|
307
|
+
raise AgentContractError("invariant_violation", "semantic child binding is missing", target=child_path)
|
|
308
|
+
visit(child, depth + 1)
|
|
309
|
+
visiting.remove(record.path)
|
|
310
|
+
|
|
311
|
+
visit(root, 0)
|
|
312
|
+
return ordered
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _make_manifest(
|
|
316
|
+
agent: HwpxAgentDocument,
|
|
317
|
+
*,
|
|
318
|
+
root_path: str,
|
|
319
|
+
source_label: str,
|
|
320
|
+
mode: str,
|
|
321
|
+
include_assets: bool,
|
|
322
|
+
require_replayable: bool,
|
|
323
|
+
) -> tuple[dict[str, Any], dict[str, bytes]]:
|
|
324
|
+
if mode not in BLUEPRINT_MODES:
|
|
325
|
+
raise AgentContractError("invalid_syntax", "unsupported blueprint mode", target="mode")
|
|
326
|
+
root_record = agent.resolve_record(root_path, expected_revision=agent.revision)
|
|
327
|
+
ordered = _ordered_subtree(agent, root_record)
|
|
328
|
+
logical_ids = {record.path: f"n{index:06d}" for index, (record, _) in enumerate(ordered, start=1)}
|
|
329
|
+
styles: dict[str, dict[str, Any]] = {}
|
|
330
|
+
numbering: dict[str, dict[str, Any]] = {}
|
|
331
|
+
resources: dict[str, dict[str, Any]] = {}
|
|
332
|
+
assets: dict[str, bytes] = {}
|
|
333
|
+
unsupported: list[dict[str, str]] = []
|
|
334
|
+
references: list[dict[str, Any]] = []
|
|
335
|
+
nodes: list[dict[str, Any]] = []
|
|
336
|
+
selected = {record.path: record for record, _depth in ordered}
|
|
337
|
+
|
|
338
|
+
for record, _depth in ordered:
|
|
339
|
+
style_refs: list[str] = []
|
|
340
|
+
numbering_refs: list[str] = []
|
|
341
|
+
resource_refs: list[str] = []
|
|
342
|
+
node_references: list[str] = []
|
|
343
|
+
dependency = _style_dependency(agent, record)
|
|
344
|
+
if dependency is not None:
|
|
345
|
+
styles.setdefault(str(dependency["key"]), dependency)
|
|
346
|
+
style_refs.append(str(dependency["key"]))
|
|
347
|
+
numbering_properties = _numbering_properties(agent, record)
|
|
348
|
+
if numbering_properties is not None:
|
|
349
|
+
definition = _dependency("numbering", numbering_properties, name=str(numbering_properties["type"]))
|
|
350
|
+
numbering.setdefault(str(definition["key"]), definition)
|
|
351
|
+
numbering_refs.append(str(definition["key"]))
|
|
352
|
+
|
|
353
|
+
node_unsupported: list[str] = []
|
|
354
|
+
for kind, count in sorted(record.unsupported_child_kinds.items()):
|
|
355
|
+
reason = f"unprojected semantic child ({count})"
|
|
356
|
+
unsupported.append({"path": record.path, "kind": str(kind), "reason": reason})
|
|
357
|
+
node_unsupported.append(reason)
|
|
358
|
+
|
|
359
|
+
if record.kind == "picture":
|
|
360
|
+
extracted = _picture_resource(agent, record) if include_assets else None
|
|
361
|
+
if extracted is None:
|
|
362
|
+
reason = "picture resource is missing, disabled, or not allow-listed"
|
|
363
|
+
unsupported.append({"path": record.path, "kind": "resource", "reason": reason})
|
|
364
|
+
node_unsupported.append(reason)
|
|
365
|
+
else:
|
|
366
|
+
resource, payload = extracted
|
|
367
|
+
resources.setdefault(str(resource["key"]), resource)
|
|
368
|
+
assets.setdefault(str(resource["assetPath"]), payload)
|
|
369
|
+
resource_refs.append(str(resource["key"]))
|
|
370
|
+
|
|
371
|
+
if record.kind == "shape":
|
|
372
|
+
shape_type = str(record.summary.get("shapeType") or "")
|
|
373
|
+
if shape_type not in {"rect", "ellipse"} or not _portable_properties(record).get("treatAsChar"):
|
|
374
|
+
reason = "only inline rectangle/ellipse shapes are portable in blueprint v1"
|
|
375
|
+
unsupported.append({"path": record.path, "kind": "shape", "reason": reason})
|
|
376
|
+
node_unsupported.append(reason)
|
|
377
|
+
|
|
378
|
+
host_run_path = _host_run_path(record, selected)
|
|
379
|
+
if host_run_path is not None:
|
|
380
|
+
references.append(
|
|
381
|
+
{
|
|
382
|
+
"from": logical_ids[record.path],
|
|
383
|
+
"field": "hostRun",
|
|
384
|
+
"to": logical_ids[host_run_path],
|
|
385
|
+
"required": True,
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
node_references.append("hostRun")
|
|
389
|
+
if record.kind == "form-field" and record.parent_path is not None:
|
|
390
|
+
paragraph = selected.get(record.parent_path)
|
|
391
|
+
run_paths = [
|
|
392
|
+
path
|
|
393
|
+
for path in (paragraph.child_paths if paragraph is not None else [])
|
|
394
|
+
if path in selected and selected[path].kind == "run"
|
|
395
|
+
]
|
|
396
|
+
for field, index_key in (("hostRun", "_begin_run_index"), ("endRun", "_end_run_index")):
|
|
397
|
+
raw_index = record.native.get(index_key)
|
|
398
|
+
if isinstance(raw_index, int) and 0 <= raw_index < len(run_paths):
|
|
399
|
+
references.append(
|
|
400
|
+
{
|
|
401
|
+
"from": logical_ids[record.path],
|
|
402
|
+
"field": field,
|
|
403
|
+
"to": logical_ids[run_paths[raw_index]],
|
|
404
|
+
"required": True,
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
node_references.append(field)
|
|
408
|
+
|
|
409
|
+
replayable = not node_unsupported
|
|
410
|
+
native_id = record.attributes.get("id") or record.stable_id
|
|
411
|
+
node = {
|
|
412
|
+
"blueprintId": logical_ids[record.path],
|
|
413
|
+
"kind": record.kind,
|
|
414
|
+
"properties": _portable_properties(record),
|
|
415
|
+
"children": [logical_ids[path] for path in record.child_paths],
|
|
416
|
+
"styleRefs": sorted(style_refs),
|
|
417
|
+
"numberingRefs": sorted(numbering_refs),
|
|
418
|
+
"resourceRefs": sorted(resource_refs),
|
|
419
|
+
"references": sorted(node_references),
|
|
420
|
+
"sourceHint": {"nativeId": native_id, "path": record.path},
|
|
421
|
+
"support": {
|
|
422
|
+
"replayable": replayable,
|
|
423
|
+
"fidelity": "exact" if replayable else "unsupported",
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
nodes.append(node)
|
|
427
|
+
|
|
428
|
+
if root_record.kind in {"cell", "form-field"}:
|
|
429
|
+
reason = f"standalone {root_record.kind} insertion is unavailable in blueprint v1"
|
|
430
|
+
unsupported.append({"path": root_record.path, "kind": root_record.kind, "reason": reason})
|
|
431
|
+
nodes[0]["support"] = {"replayable": False, "fidelity": "unsupported"}
|
|
432
|
+
all_replayable = not unsupported and all(node["support"]["replayable"] for node in nodes)
|
|
433
|
+
if require_replayable and not all_replayable:
|
|
434
|
+
first = unsupported[0] if unsupported else {"path": root_record.path, "reason": "not replayable"}
|
|
435
|
+
raise AgentContractError(
|
|
436
|
+
"unsupported_content",
|
|
437
|
+
f"strict blueprint dump is not replayable: {first['reason']}",
|
|
438
|
+
target=str(first["path"]),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
kind_order = {kind: index for index, kind in enumerate(NODE_KINDS)}
|
|
442
|
+
manifest: dict[str, Any] = {
|
|
443
|
+
"schemaVersion": BLUEPRINT_SCHEMA,
|
|
444
|
+
"catalogVersion": AGENT_CATALOG_SCHEMA,
|
|
445
|
+
"catalogHash": catalog_hash(),
|
|
446
|
+
"source": {"revision": agent.revision, "label": source_label[:512]},
|
|
447
|
+
"mode": mode,
|
|
448
|
+
"root": {
|
|
449
|
+
"blueprintId": logical_ids[root_record.path],
|
|
450
|
+
"kind": root_record.kind,
|
|
451
|
+
"sourcePath": root_record.path,
|
|
452
|
+
"sourceStability": root_record.stability,
|
|
453
|
+
},
|
|
454
|
+
"nodes": nodes,
|
|
455
|
+
"styles": [styles[key] for key in sorted(styles)],
|
|
456
|
+
"numbering": [numbering[key] for key in sorted(numbering)],
|
|
457
|
+
"resources": [resources[key] for key in sorted(resources)],
|
|
458
|
+
"references": references,
|
|
459
|
+
"unsupported": unsupported,
|
|
460
|
+
"capabilities": {
|
|
461
|
+
"dump": True,
|
|
462
|
+
"replay": all_replayable,
|
|
463
|
+
"kinds": sorted({node["kind"] for node in nodes}, key=lambda kind: kind_order[kind]),
|
|
464
|
+
},
|
|
465
|
+
"limits": blueprint_limits(),
|
|
466
|
+
"fidelity": {
|
|
467
|
+
"replayable": all_replayable,
|
|
468
|
+
"ceiling": "exact" if all_replayable else "unsupported",
|
|
469
|
+
"reasons": [item["reason"] for item in unsupported],
|
|
470
|
+
},
|
|
471
|
+
"blueprintHash": None,
|
|
472
|
+
}
|
|
473
|
+
return validate_blueprint_manifest(with_blueprint_hash(manifest)), assets
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def dump_document_blueprint(
|
|
477
|
+
source: str | os.PathLike[str] | bytes,
|
|
478
|
+
*,
|
|
479
|
+
path: str = "/",
|
|
480
|
+
mode: str = "portable",
|
|
481
|
+
expected_revision: str | None = None,
|
|
482
|
+
output: str | os.PathLike[str] | None = None,
|
|
483
|
+
overwrite: bool = False,
|
|
484
|
+
include_assets: bool = True,
|
|
485
|
+
require_replayable: bool = True,
|
|
486
|
+
) -> BlueprintDumpResult:
|
|
487
|
+
"""Dump one revision-bound semantic subtree to a validated typed bundle."""
|
|
488
|
+
|
|
489
|
+
source_label = "memory.hwpx" if isinstance(source, bytes) else Path(source).name
|
|
490
|
+
with HwpxAgentDocument.open(source) as agent:
|
|
491
|
+
if expected_revision is not None and expected_revision != agent.revision:
|
|
492
|
+
raise AgentContractError(
|
|
493
|
+
"stale_revision", "document revision does not match", target=path
|
|
494
|
+
)
|
|
495
|
+
manifest, assets = _make_manifest(
|
|
496
|
+
agent,
|
|
497
|
+
root_path=path,
|
|
498
|
+
source_label=source_label,
|
|
499
|
+
mode=mode,
|
|
500
|
+
include_assets=include_assets,
|
|
501
|
+
require_replayable=require_replayable,
|
|
502
|
+
)
|
|
503
|
+
if output is None:
|
|
504
|
+
bundle_bytes = build_blueprint_bundle(manifest, assets)
|
|
505
|
+
bundle = read_blueprint_bundle(bundle_bytes)
|
|
506
|
+
output_filename = None
|
|
507
|
+
else:
|
|
508
|
+
bundle = write_blueprint_bundle(output, manifest, assets, overwrite=overwrite)
|
|
509
|
+
bundle_bytes = Path(output).read_bytes()
|
|
510
|
+
output_filename = str(output)
|
|
511
|
+
return BlueprintDumpResult(
|
|
512
|
+
manifest=bundle.manifest,
|
|
513
|
+
assets=bundle.assets,
|
|
514
|
+
bundle_bytes=bundle_bytes,
|
|
515
|
+
bundle_sha256=bundle.bundle_sha256,
|
|
516
|
+
output_filename=output_filename,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
__all__ = ["BlueprintDumpResult", "dump_document_blueprint"]
|