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