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,647 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Unified ``hwpx`` command line for the semantic agent document interface."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import contextlib
8
+ import json
9
+ import re
10
+ import sys
11
+ from collections.abc import Mapping, Sequence
12
+ from pathlib import Path
13
+ from typing import Any, TextIO, cast
14
+
15
+ from .catalog import agent_catalog, human_help
16
+ from .blueprint import (
17
+ blueprint_catalog,
18
+ blueprint_human_help,
19
+ dump_document_blueprint,
20
+ read_blueprint_bundle,
21
+ repack_blueprint_bundle,
22
+ replay_document_blueprint,
23
+ )
24
+ from .commands import apply_document_commands
25
+ from .document import HwpxAgentDocument
26
+ from .model import (
27
+ AGENT_BATCH_SCHEMA,
28
+ AgentBatchResult,
29
+ AgentContractError,
30
+ AgentError,
31
+ VERIFICATION_REQUIREMENTS,
32
+ )
33
+
34
+ EXIT_OK = 0
35
+ EXIT_UNEXPECTED = 1
36
+ EXIT_USAGE = 2
37
+ EXIT_TARGET = 3
38
+ EXIT_CONFLICT = 4
39
+ EXIT_VERIFICATION = 5
40
+
41
+ _DEFAULT_REQUIREMENTS = ("package", "reopen", "openSafety", "semanticDiff", "bytePreservation")
42
+ _TARGET_CODES = frozenset(
43
+ {"not_found", "ambiguous_target", "volatile_target", "incompatible_parent", "unsupported_content"}
44
+ )
45
+ _CONFLICT_CODES = frozenset({"stale_revision", "identity_collision", "idempotency_conflict"})
46
+ _USAGE_CODES = frozenset(
47
+ {"invalid_syntax", "unknown_kind", "unknown_property", "unsupported_operation", "resource_limit"}
48
+ )
49
+
50
+
51
+ class CliUsageError(ValueError):
52
+ pass
53
+
54
+
55
+ class _HelpFormatter(argparse.HelpFormatter):
56
+ """Keep public CLI help byte-stable across supported Python minors."""
57
+
58
+ def __init__(self, prog: str) -> None:
59
+ super().__init__(prog, width=80)
60
+
61
+
62
+ class _Parser(argparse.ArgumentParser):
63
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
64
+ kwargs.setdefault("formatter_class", _HelpFormatter)
65
+ super().__init__(*args, **kwargs)
66
+
67
+ def error(self, message: str) -> None: # type: ignore[override]
68
+ # Python 3.10 quotes every choice while later argparse versions do not.
69
+ # Normalize only the parenthesized choice list; keep the invalid value's
70
+ # own quotes intact.
71
+ message = re.sub(r"'([^']*)'(?=, |\))", r"\1", message)
72
+ raise CliUsageError(message)
73
+
74
+
75
+ def _json_dump(value: Any, stream: TextIO, *, line: bool = False) -> None:
76
+ json.dump(value, stream, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
77
+ stream.write("\n" if line else "\n")
78
+
79
+
80
+ def _error_payload(error: AgentError) -> dict[str, Any]:
81
+ return {"ok": False, "error": error.to_dict()}
82
+
83
+
84
+ def _usage_error(message: str) -> AgentError:
85
+ return AgentError(
86
+ code="invalid_syntax",
87
+ message=message,
88
+ target="cli",
89
+ recoverability="terminal",
90
+ suggestion="Run 'hwpx help' or 'hwpx <command> --help'.",
91
+ )
92
+
93
+
94
+ def _unexpected_error(exc: BaseException) -> AgentError:
95
+ return AgentError(
96
+ code="verification_failed",
97
+ message=f"{type(exc).__name__}: {exc}"[:4096],
98
+ target="cli",
99
+ recoverability="terminal",
100
+ suggestion="Inspect the input and retry; no success should be assumed.",
101
+ )
102
+
103
+
104
+ def _exit_code(error: AgentError | None) -> int:
105
+ if error is None:
106
+ return EXIT_OK
107
+ if error.code in _USAGE_CODES:
108
+ return EXIT_USAGE
109
+ if error.code in _TARGET_CODES:
110
+ return EXIT_TARGET
111
+ if error.code in _CONFLICT_CODES:
112
+ return EXIT_CONFLICT
113
+ return EXIT_VERIFICATION
114
+
115
+
116
+ def _read_text(source: str, stdin: TextIO) -> str:
117
+ return stdin.read() if source == "-" else Path(source).read_text(encoding="utf-8")
118
+
119
+
120
+ def _read_binary(source: str, stdin: TextIO) -> bytes:
121
+ if source != "-":
122
+ return Path(source).read_bytes()
123
+ stream = getattr(stdin, "buffer", stdin)
124
+ data = stream.read()
125
+ if not isinstance(data, bytes):
126
+ raise CliUsageError("binary bundle stdin requires a binary-capable stream")
127
+ return data
128
+
129
+
130
+ def _write_binary(data: bytes, stdout: TextIO) -> None:
131
+ stream = getattr(stdout, "buffer", stdout)
132
+ try:
133
+ written = cast(Any, stream).write(data)
134
+ except TypeError as exc:
135
+ raise CliUsageError("binary bundle stdout requires a binary-capable stream") from exc
136
+ if written is not None and written != len(data):
137
+ raise OSError("short write while emitting blueprint bundle")
138
+
139
+
140
+ def _json_value(value: str, *, name: str, stdin: TextIO) -> Any:
141
+ raw = _read_text(value[1:], stdin) if value.startswith("@") else value
142
+ try:
143
+ return json.loads(raw)
144
+ except json.JSONDecodeError as exc:
145
+ raise CliUsageError(f"{name} is not valid JSON: {exc.msg}") from exc
146
+
147
+
148
+ def _quality_value(value: str, stdin: TextIO) -> Any:
149
+ if value in {"transparent", "strict"}:
150
+ return value
151
+ return _json_value(value, name="quality", stdin=stdin)
152
+
153
+
154
+ def _position_value(value: str | None, stdin: TextIO) -> dict[str, Any]:
155
+ if value is None:
156
+ return {"mode": "append"}
157
+ parsed = _json_value(value, name="position", stdin=stdin)
158
+ if not isinstance(parsed, Mapping):
159
+ raise CliUsageError("position must be a JSON object")
160
+ return dict(parsed)
161
+
162
+
163
+ def _properties_value(value: str, stdin: TextIO) -> dict[str, Any]:
164
+ parsed = _json_value(value, name="properties", stdin=stdin)
165
+ if not isinstance(parsed, Mapping):
166
+ raise CliUsageError("properties must be a JSON object")
167
+ return dict(parsed)
168
+
169
+
170
+ def _add_output_options(parser: argparse.ArgumentParser) -> None:
171
+ parser.add_argument(
172
+ "--format",
173
+ choices=("json", "jsonl", "human"),
174
+ default="json",
175
+ dest="output_format",
176
+ help="response format (default: json)",
177
+ )
178
+
179
+
180
+ def _add_mutation_options(parser: argparse.ArgumentParser) -> None:
181
+ parser.add_argument("--expected-revision")
182
+ parser.add_argument("--idempotency-key")
183
+ parser.add_argument("--dry-run", action="store_true")
184
+ parser.add_argument("--overwrite", action="store_true")
185
+ parser.add_argument("--quality", default="transparent")
186
+ parser.add_argument(
187
+ "--verify",
188
+ action="append",
189
+ choices=VERIFICATION_REQUIREMENTS,
190
+ dest="verification_requirements",
191
+ )
192
+ _add_output_options(parser)
193
+
194
+
195
+ def build_parser() -> argparse.ArgumentParser:
196
+ parser = _Parser(prog="hwpx", description="Semantic HWPX view/query/atomic-edit interface")
197
+ parser.add_argument("--version", action="version", version="python-hwpx agent interface v1")
198
+ subparsers = parser.add_subparsers(dest="command", required=True)
199
+
200
+ help_parser = subparsers.add_parser("help", help="catalog-generated interface help")
201
+ help_parser.add_argument("kind", nargs="?")
202
+ help_parser.add_argument("--json", action="store_true", dest="json_help")
203
+
204
+ for name in ("view", "get"):
205
+ read_parser = subparsers.add_parser(name, help=f"{name} a bounded semantic node")
206
+ read_parser.add_argument("filename")
207
+ if name == "get":
208
+ read_parser.add_argument("path")
209
+ read_parser.add_argument("--depth", type=int, default=2 if name == "view" else 0)
210
+ read_parser.add_argument("--child-limit", type=int, default=50)
211
+ read_parser.add_argument("--expected-revision")
212
+ _add_output_options(read_parser)
213
+
214
+ query_parser = subparsers.add_parser("query", help="run a bounded selector v1 query")
215
+ query_parser.add_argument("filename")
216
+ query_parser.add_argument("selector")
217
+ query_parser.add_argument("--limit", type=int, default=20)
218
+ query_parser.add_argument("--depth", type=int, default=0)
219
+ query_parser.add_argument("--child-limit", type=int, default=20)
220
+ query_parser.add_argument("--expected-revision")
221
+ _add_output_options(query_parser)
222
+
223
+ single_specs = {
224
+ "set": ("path",),
225
+ "add": ("parent", "kind"),
226
+ "remove": ("path",),
227
+ "move": ("path", "parent"),
228
+ "copy": ("path", "parent"),
229
+ }
230
+ for name, fields in single_specs.items():
231
+ command_parser = subparsers.add_parser(name, help=f"apply one atomic {name} command")
232
+ command_parser.add_argument("input")
233
+ command_parser.add_argument("output")
234
+ for field in fields:
235
+ command_parser.add_argument(field)
236
+ if name in {"set", "add"}:
237
+ command_parser.add_argument("--properties", required=True)
238
+ if name in {"add", "move", "copy"}:
239
+ command_parser.add_argument("--position")
240
+ _add_mutation_options(command_parser)
241
+
242
+ batch_parser = subparsers.add_parser("batch", help="apply JSON or JSONL command batches")
243
+ batch_parser.add_argument("request", help="JSON/JSONL path, or '-' for stdin")
244
+ batch_parser.add_argument("--jsonl-input", action="store_true")
245
+ batch_parser.add_argument("--input", help="envelope input when request contains command(s)")
246
+ batch_parser.add_argument("--output", help="envelope output when request contains command(s)")
247
+ _add_mutation_options(batch_parser)
248
+
249
+ dump_parser = subparsers.add_parser(
250
+ "dump",
251
+ help="dump, inspect, or safely repack a typed semantic blueprint",
252
+ description=blueprint_human_help(),
253
+ )
254
+ dump_parser.add_argument("source", nargs="?", help="source HWPX for dump")
255
+ dump_parser.add_argument("--path", default="/", help="revision-bound canonical source path")
256
+ dump_parser.add_argument("--mode", choices=("portable", "source-bound"), default="portable")
257
+ dump_parser.add_argument("--output", "-o", help=".hwpxbp path, or '-' for raw bundle stdout")
258
+ dump_parser.add_argument("--expected-revision")
259
+ dump_parser.add_argument("--overwrite", action="store_true")
260
+ dump_parser.add_argument("--no-assets", action="store_false", dest="include_assets", default=True)
261
+ dump_parser.add_argument(
262
+ "--inspection",
263
+ action="store_false",
264
+ dest="require_replayable",
265
+ default=True,
266
+ help="allow an inspection-only bundle with explicit unsupported inventory",
267
+ )
268
+ dump_parser.add_argument("--inspect", metavar="BUNDLE", help="validate and print one bundle manifest; '-' reads binary stdin")
269
+ dump_parser.add_argument("--repack", metavar="BUNDLE", help="validated source bundle for safe typed JSON repack")
270
+ dump_parser.add_argument("--manifest", help="edited manifest JSON path, or '-' for text stdin")
271
+ _add_output_options(dump_parser)
272
+
273
+ replay_parser = subparsers.add_parser(
274
+ "replay",
275
+ help="atomically replay a validated blueprint request",
276
+ description=blueprint_human_help(),
277
+ )
278
+ replay_parser.add_argument("request", help="replay request JSON path, or '-' for stdin")
279
+ _add_output_options(replay_parser)
280
+ return parser
281
+
282
+
283
+ def _node_human(node: Mapping[str, Any], *, indent: int = 0) -> str:
284
+ prefix = " " * indent
285
+ summary = node.get("summary") or {}
286
+ label = summary.get("text") or summary.get("name") or summary.get("caption") or ""
287
+ if isinstance(label, str) and len(label) > 80:
288
+ label = label[:79] + "…"
289
+ lines = [
290
+ f"{prefix}{node.get('kind')} {node.get('path')}"
291
+ + (f" — {label}" if label else "")
292
+ + (" [volatile]" if node.get("volatilePath") else "")
293
+ ]
294
+ for child in node.get("children") or ():
295
+ lines.append(_node_human(child, indent=indent + 1))
296
+ coverage = node.get("coverage") or {}
297
+ if coverage.get("unsupportedChildren") or coverage.get("truncatedChildren"):
298
+ lines.append(
299
+ f"{prefix} coverage: unsupported={coverage.get('unsupportedChildren', 0)} "
300
+ f"truncated={coverage.get('truncatedChildren', 0)}"
301
+ )
302
+ return "\n".join(lines)
303
+
304
+
305
+ def _query_human(payload: Mapping[str, Any]) -> str:
306
+ lines = [
307
+ f"selector: {payload.get('selector')}",
308
+ f"matches: {len(payload.get('nodes') or ())}"
309
+ + (" (truncated)" if payload.get("truncated") else ""),
310
+ ]
311
+ lines.extend(_node_human(node) for node in payload.get("nodes") or ())
312
+ return "\n".join(lines)
313
+
314
+
315
+ def _result_human(result: AgentBatchResult) -> str:
316
+ if not result.ok:
317
+ assert result.error is not None
318
+ return f"FAILED [{result.error.code}] {result.error.message}"
319
+ state = "DRY-RUN" if result.dry_run else "COMMITTED"
320
+ return (
321
+ f"{state}: {len(result.command_results)} command(s)\n"
322
+ f"inputRevision: {result.input_revision}\n"
323
+ f"documentRevision: {result.document_revision}\n"
324
+ f"output: {result.output_filename}"
325
+ )
326
+
327
+
328
+ def _blueprint_human(value: Mapping[str, Any]) -> str:
329
+ if not value.get("ok"):
330
+ error = value.get("error") or {}
331
+ return f"FAILED [{error.get('code', 'verification_failed')}] {error.get('message', '')}"
332
+ if value.get("schemaVersion") == "hwpx.agent-blueprint-replay-result/v1":
333
+ state = "DRY-RUN" if value.get("dryRun") else "COMMITTED"
334
+ return (
335
+ f"{state}: {value.get('rootPath') or '-'}\n"
336
+ f"blueprintHash: {value.get('blueprintHash')}\n"
337
+ f"documentRevision: {value.get('documentRevision')}\n"
338
+ f"output: {value.get('outputFilename')}"
339
+ )
340
+ return (
341
+ f"BLUEPRINT: {value.get('blueprintHash')}\n"
342
+ f"mode: {value.get('mode')}\n"
343
+ f"nodes: {value.get('nodeCount')}\n"
344
+ f"dependencies: {value.get('dependencyCount')}\n"
345
+ f"output: {value.get('outputFilename')}"
346
+ )
347
+
348
+
349
+ def _emit(value: Mapping[str, Any], output_format: str, stdout: TextIO) -> None:
350
+ if output_format in {"json", "jsonl"}:
351
+ _json_dump(value, stdout, line=output_format == "jsonl")
352
+ elif "schemaVersion" in value and "kind" in value:
353
+ stdout.write(_node_human(value) + "\n")
354
+ elif "nodes" in value:
355
+ stdout.write(_query_human(value) + "\n")
356
+ else:
357
+ stdout.write(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
358
+
359
+
360
+ def _envelope(
361
+ args: argparse.Namespace,
362
+ commands: Sequence[Mapping[str, Any]],
363
+ *,
364
+ input_filename: str | None = None,
365
+ output_filename: str | None = None,
366
+ stdin: TextIO,
367
+ ) -> dict[str, Any]:
368
+ requirements = args.verification_requirements or list(_DEFAULT_REQUIREMENTS)
369
+ return {
370
+ "schemaVersion": AGENT_BATCH_SCHEMA,
371
+ "input": {"filename": input_filename or args.input},
372
+ "output": {
373
+ "filename": output_filename or args.output,
374
+ "overwrite": bool(args.overwrite),
375
+ },
376
+ "commands": [dict(command) for command in commands],
377
+ "expectedRevision": args.expected_revision,
378
+ "idempotencyKey": args.idempotency_key,
379
+ "dryRun": bool(args.dry_run),
380
+ "quality": _quality_value(args.quality, stdin),
381
+ "verificationRequirements": list(requirements),
382
+ }
383
+
384
+
385
+ def _single_command(args: argparse.Namespace, stdin: TextIO) -> dict[str, Any]:
386
+ command: dict[str, Any] = {"commandId": "command", "op": args.command}
387
+ if hasattr(args, "path"):
388
+ command["path"] = args.path
389
+ if hasattr(args, "parent"):
390
+ command["parent"] = args.parent
391
+ if hasattr(args, "kind"):
392
+ command["kind"] = args.kind
393
+ if hasattr(args, "properties"):
394
+ command["properties"] = _properties_value(args.properties, stdin)
395
+ if hasattr(args, "position"):
396
+ command["position"] = _position_value(args.position, stdin)
397
+ return command
398
+
399
+
400
+ def _load_batch_requests(args: argparse.Namespace, stdin: TextIO) -> list[Mapping[str, Any]]:
401
+ text = _read_text(args.request, stdin)
402
+ if args.jsonl_input:
403
+ values: list[Any] = []
404
+ for line_number, line in enumerate(text.splitlines(), start=1):
405
+ if not line.strip():
406
+ continue
407
+ try:
408
+ values.append(json.loads(line))
409
+ except json.JSONDecodeError as exc:
410
+ raise CliUsageError(f"JSONL line {line_number} is invalid: {exc.msg}") from exc
411
+ else:
412
+ try:
413
+ parsed = json.loads(text)
414
+ except json.JSONDecodeError as exc:
415
+ raise CliUsageError(f"batch request is invalid JSON: {exc.msg}") from exc
416
+ values = parsed if isinstance(parsed, list) else [parsed]
417
+ if not values:
418
+ raise CliUsageError("batch request is empty")
419
+ if all(isinstance(value, Mapping) and "commandId" in value and "op" in value for value in values):
420
+ if not args.input or not args.output:
421
+ raise CliUsageError("command-only JSON/JSONL requires --input and --output")
422
+ return [
423
+ _envelope(
424
+ args,
425
+ [dict(value) for value in values],
426
+ input_filename=args.input,
427
+ output_filename=args.output,
428
+ stdin=stdin,
429
+ )
430
+ ]
431
+ if not all(isinstance(value, Mapping) for value in values):
432
+ raise CliUsageError("each batch request must be a JSON object")
433
+ return [dict(value) for value in values]
434
+
435
+
436
+ def _run_read(args: argparse.Namespace) -> Mapping[str, Any]: # type: ignore[return]
437
+ with HwpxAgentDocument.open(args.filename) as agent:
438
+ path = "/" if args.command == "view" else args.path
439
+ node = agent.get(
440
+ path,
441
+ depth=args.depth,
442
+ child_limit=args.child_limit,
443
+ expected_revision=args.expected_revision,
444
+ )
445
+ return node.to_dict()
446
+
447
+
448
+ def _run_query(args: argparse.Namespace) -> Mapping[str, Any]: # type: ignore[return]
449
+ with HwpxAgentDocument.open(args.filename) as agent:
450
+ result = agent.query(
451
+ args.selector,
452
+ limit=args.limit,
453
+ node_depth=args.depth,
454
+ child_limit=args.child_limit,
455
+ expected_revision=args.expected_revision,
456
+ )
457
+ return result.to_dict()
458
+
459
+
460
+ def _bundle_payload(bundle: Any, *, output_filename: str | None = None) -> dict[str, Any]:
461
+ manifest = dict(bundle.manifest)
462
+ return {
463
+ "ok": True,
464
+ "schemaVersion": manifest["schemaVersion"],
465
+ "blueprintHash": manifest["blueprintHash"],
466
+ "bundleSha256": bundle.bundle_sha256,
467
+ "bundleBytes": bundle.size,
468
+ "outputFilename": output_filename,
469
+ "mode": manifest["mode"],
470
+ "root": dict(manifest["root"]),
471
+ "nodeCount": len(manifest["nodes"]),
472
+ "dependencyCount": len(manifest["styles"]) + len(manifest["numbering"]) + len(manifest["resources"]),
473
+ "assetCount": len(bundle.assets),
474
+ "unsupported": list(manifest["unsupported"]),
475
+ "fidelity": dict(manifest["fidelity"]),
476
+ "manifest": manifest,
477
+ }
478
+
479
+
480
+ def _run_dump(args: argparse.Namespace, *, stdin: TextIO, stdout: TextIO) -> int:
481
+ modes = sum(value is not None for value in (args.source, args.inspect, args.repack))
482
+ if modes != 1:
483
+ raise CliUsageError("dump requires exactly one source, --inspect, or --repack mode")
484
+ if args.inspect is not None:
485
+ if args.output or args.manifest:
486
+ raise CliUsageError("--inspect does not accept --output or --manifest")
487
+ payload = _bundle_payload(read_blueprint_bundle(_read_binary(args.inspect, stdin)))
488
+ elif args.repack is not None:
489
+ if not args.manifest or not args.output or args.output == "-":
490
+ raise CliUsageError("--repack requires --manifest and a file --output")
491
+ manifest = _json_value(f"@{args.manifest}" if args.manifest != "-" else "@-", name="manifest", stdin=stdin)
492
+ if not isinstance(manifest, Mapping):
493
+ raise CliUsageError("manifest must be a JSON object")
494
+ bundle = repack_blueprint_bundle(
495
+ args.repack,
496
+ args.output,
497
+ dict(manifest),
498
+ overwrite=args.overwrite,
499
+ )
500
+ payload = _bundle_payload(bundle, output_filename=str(args.output))
501
+ else:
502
+ if args.manifest:
503
+ raise CliUsageError("--manifest is valid only with --repack")
504
+ output = None if args.output in {None, "-"} else args.output
505
+ result = dump_document_blueprint(
506
+ args.source,
507
+ path=args.path,
508
+ mode=args.mode,
509
+ expected_revision=args.expected_revision,
510
+ output=output,
511
+ overwrite=args.overwrite,
512
+ include_assets=args.include_assets,
513
+ require_replayable=args.require_replayable,
514
+ )
515
+ if args.output == "-":
516
+ _write_binary(result.bundle_bytes, stdout)
517
+ return EXIT_OK
518
+ payload = result.to_dict()
519
+ if args.output_format == "human":
520
+ stdout.write(_blueprint_human(payload) + "\n")
521
+ else:
522
+ _json_dump(payload, stdout, line=args.output_format == "jsonl")
523
+ return EXIT_OK
524
+
525
+
526
+ def _run_replay(args: argparse.Namespace, *, stdin: TextIO, stdout: TextIO) -> int:
527
+ request = _json_value(f"@{args.request}", name="replay request", stdin=stdin)
528
+ if not isinstance(request, Mapping):
529
+ raise CliUsageError("replay request must be a JSON object")
530
+ result = replay_document_blueprint(dict(request), idempotency_store={})
531
+ payload = result.to_dict()
532
+ if args.output_format == "human":
533
+ stdout.write(_blueprint_human(payload) + "\n")
534
+ else:
535
+ _json_dump(payload, stdout, line=args.output_format == "jsonl")
536
+ return _exit_code(result.error)
537
+
538
+
539
+ def _run_mutation(
540
+ args: argparse.Namespace,
541
+ *,
542
+ stdin: TextIO,
543
+ stdout: TextIO,
544
+ ) -> int:
545
+ store: dict[str, Any] = {}
546
+ if args.command == "batch":
547
+ requests = _load_batch_requests(args, stdin)
548
+ else:
549
+ requests = [_envelope(args, [_single_command(args, stdin)], stdin=stdin)]
550
+ exit_code = EXIT_OK
551
+ results: list[AgentBatchResult] = []
552
+ for request in requests:
553
+ result = apply_document_commands(request, idempotency_store=store)
554
+ results.append(result)
555
+ exit_code = max(exit_code, _exit_code(result.error))
556
+ if args.output_format == "human":
557
+ for result in results:
558
+ stdout.write(_result_human(result) + "\n")
559
+ elif args.output_format == "jsonl":
560
+ for result in results:
561
+ _json_dump(result.to_dict(), stdout, line=True)
562
+ else:
563
+ payload: Any = results[0].to_dict() if len(results) == 1 else [result.to_dict() for result in results]
564
+ _json_dump(payload, stdout)
565
+ return exit_code
566
+
567
+
568
+ def main(
569
+ argv: Sequence[str] | None = None,
570
+ *,
571
+ stdin: TextIO | None = None,
572
+ stdout: TextIO | None = None,
573
+ stderr: TextIO | None = None,
574
+ ) -> int:
575
+ stdin = cast(TextIO, stdin or sys.stdin)
576
+ stdout = cast(TextIO, stdout or sys.stdout)
577
+ stderr = cast(TextIO, stderr or sys.stderr)
578
+ parser = build_parser()
579
+ try:
580
+ with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
581
+ args = parser.parse_args(list(argv) if argv is not None else None)
582
+ if args.command == "help":
583
+ if args.kind == "blueprint":
584
+ if args.json_help:
585
+ _json_dump(blueprint_catalog(), stdout)
586
+ else:
587
+ stdout.write(blueprint_human_help())
588
+ return EXIT_OK
589
+ if args.json_help:
590
+ payload = agent_catalog() if args.kind is None else {
591
+ "schemaVersion": agent_catalog()["schemaVersion"],
592
+ "nodeKinds": {args.kind: agent_catalog()["nodeKinds"][args.kind]},
593
+ }
594
+ _json_dump(payload, stdout)
595
+ else:
596
+ stdout.write(human_help(args.kind))
597
+ return EXIT_OK
598
+ if args.command in {"view", "get"}:
599
+ _emit(_run_read(args), args.output_format, stdout)
600
+ return EXIT_OK
601
+ if args.command == "query":
602
+ _emit(_run_query(args), args.output_format, stdout)
603
+ return EXIT_OK
604
+ if args.command == "dump":
605
+ return _run_dump(args, stdin=stdin, stdout=stdout)
606
+ if args.command == "replay":
607
+ return _run_replay(args, stdin=stdin, stdout=stdout)
608
+ return _run_mutation(args, stdin=stdin, stdout=stdout)
609
+ except CliUsageError as exc:
610
+ error = _usage_error(str(exc))
611
+ _json_dump(_error_payload(error), stderr)
612
+ return EXIT_USAGE
613
+ except AgentContractError as exc:
614
+ error = AgentError(
615
+ code=exc.code,
616
+ message=str(exc),
617
+ target=exc.target,
618
+ recoverability="needs-review" if exc.code in _TARGET_CODES else "terminal",
619
+ )
620
+ _json_dump(_error_payload(error), stderr)
621
+ return _exit_code(error)
622
+ except (KeyError, OSError, ValueError) as exc:
623
+ error = _usage_error(str(exc)) if isinstance(exc, (KeyError, ValueError)) else _unexpected_error(exc)
624
+ _json_dump(_error_payload(error), stderr)
625
+ return _exit_code(error)
626
+ except SystemExit as exc:
627
+ return int(exc.code or 0)
628
+ except BaseException as exc: # pragma: no cover - terminal fail-closed guard
629
+ error = _unexpected_error(exc)
630
+ _json_dump(_error_payload(error), stderr)
631
+ return EXIT_UNEXPECTED
632
+
633
+
634
+ if __name__ == "__main__": # pragma: no cover
635
+ raise SystemExit(main())
636
+
637
+
638
+ __all__ = [
639
+ "EXIT_CONFLICT",
640
+ "EXIT_OK",
641
+ "EXIT_TARGET",
642
+ "EXIT_UNEXPECTED",
643
+ "EXIT_USAGE",
644
+ "EXIT_VERIFICATION",
645
+ "build_parser",
646
+ "main",
647
+ ]