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,808 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Strict v1 contracts for the semantic HWPX agent interface.
|
|
3
|
+
|
|
4
|
+
This module intentionally defines only public, JSON-serialisable semantics. It
|
|
5
|
+
does not expose lxml elements, package part paths, namespace URIs, XPath, or a
|
|
6
|
+
generic attribute mutation escape hatch. Projection, path evaluation, query,
|
|
7
|
+
and mutation execution are implemented in neighbouring modules after these
|
|
8
|
+
contracts are frozen.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Mapping, Sequence
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from hwpx.mutation_report import (
|
|
19
|
+
ChangedPart,
|
|
20
|
+
MutationReport,
|
|
21
|
+
PreservationCounts,
|
|
22
|
+
PreservationSummary,
|
|
23
|
+
verification_from_open_safety,
|
|
24
|
+
visual_value_from_status,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
AGENT_NODE_SCHEMA = "hwpx.agent-node/v1"
|
|
28
|
+
AGENT_COMMAND_SCHEMA = "hwpx.agent-command/v1"
|
|
29
|
+
AGENT_BATCH_SCHEMA = "hwpx.agent-batch/v1"
|
|
30
|
+
AGENT_BATCH_RESULT_SCHEMA = "hwpx.agent-batch-result/v1"
|
|
31
|
+
AGENT_ERROR_SCHEMA = "hwpx.agent-error/v1"
|
|
32
|
+
AGENT_CATALOG_SCHEMA = "hwpx.agent-catalog/v1"
|
|
33
|
+
|
|
34
|
+
NODE_KINDS = (
|
|
35
|
+
"document",
|
|
36
|
+
"section",
|
|
37
|
+
"paragraph",
|
|
38
|
+
"run",
|
|
39
|
+
"table",
|
|
40
|
+
"row",
|
|
41
|
+
"cell",
|
|
42
|
+
"form-field",
|
|
43
|
+
"picture",
|
|
44
|
+
"memo",
|
|
45
|
+
"footnote",
|
|
46
|
+
"endnote",
|
|
47
|
+
"shape",
|
|
48
|
+
"unsupported",
|
|
49
|
+
)
|
|
50
|
+
STABILITY_LEVELS = ("native", "derived", "positional")
|
|
51
|
+
AGENT_OPERATIONS = ("set", "add", "remove", "move", "copy")
|
|
52
|
+
|
|
53
|
+
SELECTOR_KINDS = tuple(kind for kind in NODE_KINDS if kind != "unsupported")
|
|
54
|
+
SELECTOR_ATTRIBUTES = ("id", "name", "style", "type")
|
|
55
|
+
SELECTOR_FEATURES = (
|
|
56
|
+
"kind",
|
|
57
|
+
"exact-attribute",
|
|
58
|
+
"contains",
|
|
59
|
+
"direct-child",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
MAX_VIEW_DEPTH = 8
|
|
63
|
+
MAX_CHILDREN_PER_NODE = 200
|
|
64
|
+
MAX_QUERY_RESULTS = 100
|
|
65
|
+
MAX_TEXT_CHARS = 4096
|
|
66
|
+
MAX_SELECTOR_CHARS = 512
|
|
67
|
+
MAX_COMMANDS = 100
|
|
68
|
+
MAX_PROPERTIES_PER_COMMAND = 32
|
|
69
|
+
MAX_JSON_DEPTH = 8
|
|
70
|
+
|
|
71
|
+
REVISION_PATTERN = re.compile(r"^sha256:[a-f0-9]{64}$")
|
|
72
|
+
COMMAND_ID_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,31}$")
|
|
73
|
+
COMMAND_REF_PATTERN = re.compile(
|
|
74
|
+
r"^\$([A-Za-z][A-Za-z0-9_-]{0,31})\.(path|parentPath)$"
|
|
75
|
+
)
|
|
76
|
+
CANONICAL_PATH_PATTERN = re.compile(r"^/(?:[^\x00-\x1f]*)$")
|
|
77
|
+
|
|
78
|
+
ERROR_CODES = (
|
|
79
|
+
"invalid_syntax",
|
|
80
|
+
"unknown_kind",
|
|
81
|
+
"unknown_property",
|
|
82
|
+
"unsupported_operation",
|
|
83
|
+
"not_found",
|
|
84
|
+
"ambiguous_target",
|
|
85
|
+
"stale_revision",
|
|
86
|
+
"volatile_target",
|
|
87
|
+
"incompatible_parent",
|
|
88
|
+
"identity_collision",
|
|
89
|
+
"invariant_violation",
|
|
90
|
+
"unsupported_content",
|
|
91
|
+
"resource_limit",
|
|
92
|
+
"verification_failed",
|
|
93
|
+
"idempotency_conflict",
|
|
94
|
+
)
|
|
95
|
+
RECOVERABILITY = ("retryable", "needs-review", "terminal")
|
|
96
|
+
|
|
97
|
+
VERIFICATION_REQUIREMENTS = (
|
|
98
|
+
"package",
|
|
99
|
+
"reopen",
|
|
100
|
+
"openSafety",
|
|
101
|
+
"semanticDiff",
|
|
102
|
+
"bytePreservation",
|
|
103
|
+
"domain",
|
|
104
|
+
"realHancom",
|
|
105
|
+
)
|
|
106
|
+
QUALITY_MODES = ("transparent", "strict")
|
|
107
|
+
QUALITY_KEYS = frozenset(
|
|
108
|
+
{
|
|
109
|
+
"mode",
|
|
110
|
+
"renderCheck",
|
|
111
|
+
"xsdMode",
|
|
112
|
+
"overflowPolicy",
|
|
113
|
+
"layoutLint",
|
|
114
|
+
"preserveUnmodifiedParts",
|
|
115
|
+
"requireReferenceIntegrity",
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
_QUALITY_ENUM_VALUES = {
|
|
119
|
+
"mode": QUALITY_MODES,
|
|
120
|
+
"renderCheck": ("off", "auto", "required"),
|
|
121
|
+
"xsdMode": ("off", "lint"),
|
|
122
|
+
"overflowPolicy": ("fail", "warn", "truncate"),
|
|
123
|
+
"layoutLint": ("off", "warn", "strict"),
|
|
124
|
+
}
|
|
125
|
+
_QUALITY_BOOLEAN_KEYS = frozenset(
|
|
126
|
+
{"preserveUnmodifiedParts", "requireReferenceIntegrity"}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# The v1 public property vocabulary. P2's command catalog consumes this exact
|
|
130
|
+
# manifest. A property absent here cannot be set through generic commands.
|
|
131
|
+
NODE_PROPERTY_CATALOG_V1: dict[str, dict[str, tuple[str, ...]]] = {
|
|
132
|
+
"document": {
|
|
133
|
+
"readable": ("title", "author", "sectionCount", "paragraphCount", "tableCount"),
|
|
134
|
+
"editable": (),
|
|
135
|
+
"operations": ("add",),
|
|
136
|
+
},
|
|
137
|
+
"section": {
|
|
138
|
+
"readable": ("index", "partId", "paragraphCount", "pageWidthMm", "pageHeightMm"),
|
|
139
|
+
"editable": (),
|
|
140
|
+
"operations": ("add",),
|
|
141
|
+
},
|
|
142
|
+
"paragraph": {
|
|
143
|
+
"readable": (
|
|
144
|
+
"text",
|
|
145
|
+
"style",
|
|
146
|
+
"alignment",
|
|
147
|
+
"breakBefore",
|
|
148
|
+
"keepWithNext",
|
|
149
|
+
"lineSpacingPercent",
|
|
150
|
+
),
|
|
151
|
+
"editable": (
|
|
152
|
+
"text",
|
|
153
|
+
"style",
|
|
154
|
+
"alignment",
|
|
155
|
+
"breakBefore",
|
|
156
|
+
"keepWithNext",
|
|
157
|
+
"lineSpacingPercent",
|
|
158
|
+
),
|
|
159
|
+
"operations": AGENT_OPERATIONS,
|
|
160
|
+
},
|
|
161
|
+
"run": {
|
|
162
|
+
"readable": ("text", "bold", "italic", "underline", "fontName", "fontSizePt", "color"),
|
|
163
|
+
"editable": ("text", "bold", "italic", "underline", "fontName", "fontSizePt", "color"),
|
|
164
|
+
"operations": ("set", "add", "remove", "copy"),
|
|
165
|
+
},
|
|
166
|
+
"table": {
|
|
167
|
+
"readable": ("rowCount", "columnCount", "caption", "widthMm", "alignment"),
|
|
168
|
+
"editable": ("caption", "alignment"),
|
|
169
|
+
"operations": AGENT_OPERATIONS,
|
|
170
|
+
},
|
|
171
|
+
"row": {
|
|
172
|
+
"readable": ("index", "cellCount", "heightMm"),
|
|
173
|
+
"editable": ("heightMm",),
|
|
174
|
+
"operations": ("set", "add", "remove", "move", "copy"),
|
|
175
|
+
},
|
|
176
|
+
"cell": {
|
|
177
|
+
"readable": (
|
|
178
|
+
"text",
|
|
179
|
+
"row",
|
|
180
|
+
"column",
|
|
181
|
+
"rowSpan",
|
|
182
|
+
"columnSpan",
|
|
183
|
+
"verticalAlignment",
|
|
184
|
+
"backgroundColor",
|
|
185
|
+
),
|
|
186
|
+
"editable": ("text", "verticalAlignment", "backgroundColor"),
|
|
187
|
+
"operations": ("set",),
|
|
188
|
+
},
|
|
189
|
+
"form-field": {
|
|
190
|
+
"readable": ("name", "value", "fieldType", "readOnly"),
|
|
191
|
+
"editable": ("value", "readOnly"),
|
|
192
|
+
"operations": ("set",),
|
|
193
|
+
},
|
|
194
|
+
"picture": {
|
|
195
|
+
"readable": ("name", "altText", "widthMm", "heightMm", "mediaType"),
|
|
196
|
+
"editable": ("altText",),
|
|
197
|
+
"operations": ("set", "remove", "move", "copy"),
|
|
198
|
+
},
|
|
199
|
+
"memo": {
|
|
200
|
+
"readable": ("text", "author"),
|
|
201
|
+
"editable": ("text",),
|
|
202
|
+
"operations": ("set", "remove", "copy"),
|
|
203
|
+
},
|
|
204
|
+
"footnote": {
|
|
205
|
+
"readable": ("text",),
|
|
206
|
+
"editable": ("text",),
|
|
207
|
+
"operations": ("set", "remove", "copy"),
|
|
208
|
+
},
|
|
209
|
+
"endnote": {
|
|
210
|
+
"readable": ("text",),
|
|
211
|
+
"editable": ("text",),
|
|
212
|
+
"operations": ("set", "remove", "copy"),
|
|
213
|
+
},
|
|
214
|
+
"shape": {
|
|
215
|
+
"readable": ("shapeType", "name", "altText", "xMm", "yMm", "widthMm", "heightMm"),
|
|
216
|
+
"editable": ("altText",),
|
|
217
|
+
"operations": ("set", "remove", "move", "copy"),
|
|
218
|
+
},
|
|
219
|
+
"unsupported": {"readable": ("localName", "reason"), "editable": (), "operations": ()},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_RAW_KEYS = frozenset(
|
|
223
|
+
{"xml", "raw", "rawxml", "xpath", "namespace", "namespaceuri", "packagepath", "partpath"}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class AgentContractError(ValueError):
|
|
228
|
+
"""A stable-code contract validation error."""
|
|
229
|
+
|
|
230
|
+
def __init__(self, code: str, message: str, *, target: str | None = None) -> None:
|
|
231
|
+
if code not in ERROR_CODES:
|
|
232
|
+
raise ValueError(f"unknown agent error code: {code}")
|
|
233
|
+
super().__init__(message)
|
|
234
|
+
self.code = code
|
|
235
|
+
self.target = target
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _require_mapping(value: object, name: str) -> Mapping[str, Any]:
|
|
239
|
+
if not isinstance(value, Mapping):
|
|
240
|
+
raise AgentContractError("invalid_syntax", f"{name} must be an object", target=name)
|
|
241
|
+
return value
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _require_exact_keys(
|
|
245
|
+
value: Mapping[str, Any],
|
|
246
|
+
*,
|
|
247
|
+
required: set[str] | frozenset[str],
|
|
248
|
+
optional: set[str] | frozenset[str] = frozenset(),
|
|
249
|
+
name: str,
|
|
250
|
+
) -> None:
|
|
251
|
+
missing = required - set(value)
|
|
252
|
+
extra = set(value) - required - optional
|
|
253
|
+
if missing or extra:
|
|
254
|
+
raise AgentContractError(
|
|
255
|
+
"invalid_syntax",
|
|
256
|
+
f"{name} fields mismatch (missing={sorted(missing)}, extra={sorted(extra)})",
|
|
257
|
+
target=name,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _validate_json_value(value: object, *, name: str, depth: int = 0) -> None:
|
|
262
|
+
if depth > MAX_JSON_DEPTH:
|
|
263
|
+
raise AgentContractError("resource_limit", f"{name} exceeds JSON depth limit", target=name)
|
|
264
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
265
|
+
return
|
|
266
|
+
if isinstance(value, str):
|
|
267
|
+
if len(value) > MAX_TEXT_CHARS:
|
|
268
|
+
raise AgentContractError("resource_limit", f"{name} string is too long", target=name)
|
|
269
|
+
return
|
|
270
|
+
if isinstance(value, Mapping):
|
|
271
|
+
for key, child in value.items():
|
|
272
|
+
key_text = str(key)
|
|
273
|
+
normalized = re.sub(r"[^a-z]", "", key_text.casefold())
|
|
274
|
+
if normalized in _RAW_KEYS:
|
|
275
|
+
raise AgentContractError(
|
|
276
|
+
"unknown_property",
|
|
277
|
+
f"{name} contains forbidden raw/package property {key_text!r}",
|
|
278
|
+
target=f"{name}.{key_text}",
|
|
279
|
+
)
|
|
280
|
+
_validate_json_value(child, name=f"{name}.{key_text}", depth=depth + 1)
|
|
281
|
+
return
|
|
282
|
+
if isinstance(value, (list, tuple)):
|
|
283
|
+
for index, child in enumerate(value):
|
|
284
|
+
_validate_json_value(child, name=f"{name}[{index}]", depth=depth + 1)
|
|
285
|
+
return
|
|
286
|
+
raise AgentContractError(
|
|
287
|
+
"invalid_syntax", f"{name} is not JSON-serialisable", target=name
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _validate_path_or_reference(value: object, name: str) -> str:
|
|
292
|
+
text = str(value or "")
|
|
293
|
+
if COMMAND_REF_PATTERN.fullmatch(text):
|
|
294
|
+
return text
|
|
295
|
+
if not CANONICAL_PATH_PATTERN.fullmatch(text):
|
|
296
|
+
raise AgentContractError(
|
|
297
|
+
"invalid_syntax", f"{name} must be an absolute semantic path or command reference", target=name
|
|
298
|
+
)
|
|
299
|
+
if len(text) > MAX_SELECTOR_CHARS * 2:
|
|
300
|
+
raise AgentContractError("resource_limit", f"{name} is too long", target=name)
|
|
301
|
+
return text
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _validate_properties(value: object, name: str) -> dict[str, Any]:
|
|
305
|
+
props = dict(_require_mapping(value, name))
|
|
306
|
+
if not props:
|
|
307
|
+
raise AgentContractError("invalid_syntax", f"{name} cannot be empty", target=name)
|
|
308
|
+
if len(props) > MAX_PROPERTIES_PER_COMMAND:
|
|
309
|
+
raise AgentContractError("resource_limit", f"{name} has too many properties", target=name)
|
|
310
|
+
_validate_json_value(props, name=name)
|
|
311
|
+
return props
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _validate_position(value: object, name: str = "command.position") -> dict[str, Any]:
|
|
315
|
+
position = dict(_require_mapping(value, name))
|
|
316
|
+
mode = str(position.get("mode", ""))
|
|
317
|
+
if mode in {"append", "prepend"}:
|
|
318
|
+
_require_exact_keys(position, required={"mode"}, name=name)
|
|
319
|
+
return {"mode": mode}
|
|
320
|
+
if mode == "index":
|
|
321
|
+
_require_exact_keys(position, required={"mode", "index"}, name=name)
|
|
322
|
+
index = position["index"]
|
|
323
|
+
if isinstance(index, bool) or not isinstance(index, int) or index < 1:
|
|
324
|
+
raise AgentContractError(
|
|
325
|
+
"invalid_syntax", f"{name}.index must be a one-based integer", target=f"{name}.index"
|
|
326
|
+
)
|
|
327
|
+
return {"mode": mode, "index": index}
|
|
328
|
+
if mode in {"before", "after"}:
|
|
329
|
+
_require_exact_keys(position, required={"mode", "path"}, name=name)
|
|
330
|
+
return {"mode": mode, "path": _validate_path_or_reference(position["path"], f"{name}.path")}
|
|
331
|
+
raise AgentContractError(
|
|
332
|
+
"invalid_syntax", f"{name}.mode must be append, prepend, index, before, or after", target=f"{name}.mode"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _validate_quality(value: object) -> str | dict[str, Any] | None:
|
|
337
|
+
if value is None:
|
|
338
|
+
return None
|
|
339
|
+
if isinstance(value, str):
|
|
340
|
+
if value not in QUALITY_MODES:
|
|
341
|
+
raise AgentContractError("invalid_syntax", "batch.quality mode is unsupported", target="batch.quality")
|
|
342
|
+
return value
|
|
343
|
+
quality = dict(_require_mapping(value, "batch.quality"))
|
|
344
|
+
extra = set(quality) - QUALITY_KEYS
|
|
345
|
+
if extra:
|
|
346
|
+
raise AgentContractError(
|
|
347
|
+
"unknown_property",
|
|
348
|
+
f"batch.quality contains unknown fields: {sorted(extra)}",
|
|
349
|
+
target="batch.quality",
|
|
350
|
+
)
|
|
351
|
+
for name, choices in _QUALITY_ENUM_VALUES.items():
|
|
352
|
+
if name in quality and quality[name] not in choices:
|
|
353
|
+
raise AgentContractError(
|
|
354
|
+
"invalid_syntax",
|
|
355
|
+
f"batch.quality.{name} is unsupported",
|
|
356
|
+
target=f"batch.quality.{name}",
|
|
357
|
+
)
|
|
358
|
+
for name in _QUALITY_BOOLEAN_KEYS:
|
|
359
|
+
if name in quality and not isinstance(quality[name], bool):
|
|
360
|
+
raise AgentContractError(
|
|
361
|
+
"invalid_syntax",
|
|
362
|
+
f"batch.quality.{name} must be boolean",
|
|
363
|
+
target=f"batch.quality.{name}",
|
|
364
|
+
)
|
|
365
|
+
_validate_json_value(quality, name="batch.quality")
|
|
366
|
+
return quality
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@dataclass(frozen=True, slots=True)
|
|
370
|
+
class AgentNode:
|
|
371
|
+
"""A bounded semantic node resolved against one document revision."""
|
|
372
|
+
|
|
373
|
+
kind: str
|
|
374
|
+
path: str
|
|
375
|
+
stable_id: str | None
|
|
376
|
+
stability: str
|
|
377
|
+
summary: Mapping[str, Any]
|
|
378
|
+
child_count: int
|
|
379
|
+
children: tuple["AgentNode", ...] = ()
|
|
380
|
+
unsupported_child_count: int = 0
|
|
381
|
+
truncated_child_count: int = 0
|
|
382
|
+
readable_properties: tuple[str, ...] = ()
|
|
383
|
+
editable_properties: tuple[str, ...] = ()
|
|
384
|
+
operations: tuple[str, ...] = ()
|
|
385
|
+
revision: str = ""
|
|
386
|
+
|
|
387
|
+
def __post_init__(self) -> None:
|
|
388
|
+
if self.kind not in NODE_KINDS:
|
|
389
|
+
raise AgentContractError("unknown_kind", f"unknown node kind: {self.kind}", target="node.kind")
|
|
390
|
+
if not CANONICAL_PATH_PATTERN.fullmatch(self.path):
|
|
391
|
+
raise AgentContractError(
|
|
392
|
+
"invalid_syntax", "node.path must be an absolute canonical path", target="node.path"
|
|
393
|
+
)
|
|
394
|
+
if self.stability not in STABILITY_LEVELS:
|
|
395
|
+
raise AgentContractError("invalid_syntax", "unknown node stability", target="node.stability")
|
|
396
|
+
if self.stability == "positional" and self.stable_id is not None:
|
|
397
|
+
raise AgentContractError(
|
|
398
|
+
"invariant_violation", "positional nodes cannot claim stableId", target="node.stableId"
|
|
399
|
+
)
|
|
400
|
+
if self.stability != "positional" and not self.stable_id:
|
|
401
|
+
raise AgentContractError(
|
|
402
|
+
"invariant_violation", "native/derived nodes require stableId", target="node.stableId"
|
|
403
|
+
)
|
|
404
|
+
if self.stable_id is not None and (not self.stable_id or len(self.stable_id) > 256):
|
|
405
|
+
raise AgentContractError("resource_limit", "node.stableId is invalid", target="node.stableId")
|
|
406
|
+
for name, value in (
|
|
407
|
+
("childCount", self.child_count),
|
|
408
|
+
("unsupportedChildCount", self.unsupported_child_count),
|
|
409
|
+
("truncatedChildCount", self.truncated_child_count),
|
|
410
|
+
):
|
|
411
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
412
|
+
raise AgentContractError("invalid_syntax", f"node.{name} must be non-negative", target=f"node.{name}")
|
|
413
|
+
if len(self.children) > MAX_CHILDREN_PER_NODE:
|
|
414
|
+
raise AgentContractError("resource_limit", "node children exceed limit", target="node.children")
|
|
415
|
+
if (
|
|
416
|
+
len(self.children)
|
|
417
|
+
+ self.unsupported_child_count
|
|
418
|
+
+ self.truncated_child_count
|
|
419
|
+
!= self.child_count
|
|
420
|
+
):
|
|
421
|
+
raise AgentContractError(
|
|
422
|
+
"invariant_violation", "node child coverage must equal childCount", target="node.children"
|
|
423
|
+
)
|
|
424
|
+
if not REVISION_PATTERN.fullmatch(self.revision):
|
|
425
|
+
raise AgentContractError("invalid_syntax", "node.revision must be sha256", target="node.revision")
|
|
426
|
+
_validate_json_value(self.summary, name="node.summary")
|
|
427
|
+
catalog = NODE_PROPERTY_CATALOG_V1[self.kind]
|
|
428
|
+
if tuple(self.readable_properties) != tuple(catalog["readable"]):
|
|
429
|
+
raise AgentContractError(
|
|
430
|
+
"invariant_violation", "node readable properties do not match v1 catalog", target="node.readableProperties"
|
|
431
|
+
)
|
|
432
|
+
if tuple(self.editable_properties) != tuple(catalog["editable"]):
|
|
433
|
+
raise AgentContractError(
|
|
434
|
+
"invariant_violation", "node editable properties do not match v1 catalog", target="node.editableProperties"
|
|
435
|
+
)
|
|
436
|
+
if tuple(self.operations) != tuple(catalog["operations"]):
|
|
437
|
+
raise AgentContractError(
|
|
438
|
+
"invariant_violation", "node operations do not match v1 catalog", target="node.operations"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def volatile_path(self) -> bool:
|
|
443
|
+
return self.stability == "positional"
|
|
444
|
+
|
|
445
|
+
def to_dict(self) -> dict[str, Any]:
|
|
446
|
+
return {
|
|
447
|
+
"schemaVersion": AGENT_NODE_SCHEMA,
|
|
448
|
+
"kind": self.kind,
|
|
449
|
+
"path": self.path,
|
|
450
|
+
"stableId": self.stable_id,
|
|
451
|
+
"stability": self.stability,
|
|
452
|
+
"volatilePath": self.volatile_path,
|
|
453
|
+
"summary": dict(self.summary),
|
|
454
|
+
"childCount": self.child_count,
|
|
455
|
+
"children": [child.to_dict() for child in self.children],
|
|
456
|
+
"coverage": {
|
|
457
|
+
"supportedChildren": len(self.children),
|
|
458
|
+
"unsupportedChildren": self.unsupported_child_count,
|
|
459
|
+
"truncatedChildren": self.truncated_child_count,
|
|
460
|
+
},
|
|
461
|
+
"readableProperties": list(self.readable_properties),
|
|
462
|
+
"editableProperties": list(self.editable_properties),
|
|
463
|
+
"operations": list(self.operations),
|
|
464
|
+
"revision": self.revision,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass(frozen=True, slots=True)
|
|
469
|
+
class AgentError:
|
|
470
|
+
code: str
|
|
471
|
+
message: str
|
|
472
|
+
target: str | None = None
|
|
473
|
+
recoverability: str = "terminal"
|
|
474
|
+
suggestion: str | None = None
|
|
475
|
+
valid_values: tuple[str, ...] = ()
|
|
476
|
+
|
|
477
|
+
def __post_init__(self) -> None:
|
|
478
|
+
if self.code not in ERROR_CODES:
|
|
479
|
+
raise ValueError(f"unknown agent error code: {self.code}")
|
|
480
|
+
if self.recoverability not in RECOVERABILITY:
|
|
481
|
+
raise ValueError(f"unknown recoverability: {self.recoverability}")
|
|
482
|
+
if not self.message or len(self.message) > MAX_TEXT_CHARS:
|
|
483
|
+
raise ValueError("agent error message is empty or too long")
|
|
484
|
+
|
|
485
|
+
def to_dict(self) -> dict[str, Any]:
|
|
486
|
+
return {
|
|
487
|
+
"schemaVersion": AGENT_ERROR_SCHEMA,
|
|
488
|
+
"code": self.code,
|
|
489
|
+
"message": self.message,
|
|
490
|
+
"target": self.target,
|
|
491
|
+
"recoverability": self.recoverability,
|
|
492
|
+
"suggestion": self.suggestion,
|
|
493
|
+
"validValues": list(self.valid_values),
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def validate_agent_command(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
498
|
+
"""Validate and normalize one strict op-specific command union."""
|
|
499
|
+
|
|
500
|
+
raw = dict(_require_mapping(value, "command"))
|
|
501
|
+
command_id = str(raw.get("commandId", ""))
|
|
502
|
+
if not COMMAND_ID_PATTERN.fullmatch(command_id):
|
|
503
|
+
raise AgentContractError("invalid_syntax", "commandId is invalid", target="command.commandId")
|
|
504
|
+
op = str(raw.get("op", ""))
|
|
505
|
+
if op not in AGENT_OPERATIONS:
|
|
506
|
+
raise AgentContractError("unsupported_operation", f"unsupported command op: {op}", target="command.op")
|
|
507
|
+
|
|
508
|
+
normalized: dict[str, Any] = {
|
|
509
|
+
"schemaVersion": AGENT_COMMAND_SCHEMA,
|
|
510
|
+
"commandId": command_id,
|
|
511
|
+
"op": op,
|
|
512
|
+
}
|
|
513
|
+
if op == "set":
|
|
514
|
+
_require_exact_keys(raw, required={"commandId", "op", "path", "properties"}, name="command")
|
|
515
|
+
normalized["path"] = _validate_path_or_reference(raw["path"], "command.path")
|
|
516
|
+
normalized["properties"] = _validate_properties(raw["properties"], "command.properties")
|
|
517
|
+
elif op == "add":
|
|
518
|
+
_require_exact_keys(
|
|
519
|
+
raw,
|
|
520
|
+
required={"commandId", "op", "parent", "kind", "properties"},
|
|
521
|
+
optional={"position"},
|
|
522
|
+
name="command",
|
|
523
|
+
)
|
|
524
|
+
kind = str(raw["kind"])
|
|
525
|
+
if kind not in NODE_KINDS or kind in {"document", "unsupported"}:
|
|
526
|
+
raise AgentContractError("unknown_kind", f"cannot add node kind: {kind}", target="command.kind")
|
|
527
|
+
normalized["parent"] = _validate_path_or_reference(raw["parent"], "command.parent")
|
|
528
|
+
normalized["kind"] = kind
|
|
529
|
+
normalized["properties"] = _validate_properties(raw["properties"], "command.properties")
|
|
530
|
+
normalized["position"] = _validate_position(raw.get("position", {"mode": "append"}))
|
|
531
|
+
elif op == "remove":
|
|
532
|
+
_require_exact_keys(raw, required={"commandId", "op", "path"}, name="command")
|
|
533
|
+
normalized["path"] = _validate_path_or_reference(raw["path"], "command.path")
|
|
534
|
+
else:
|
|
535
|
+
_require_exact_keys(
|
|
536
|
+
raw,
|
|
537
|
+
required={"commandId", "op", "path", "parent"},
|
|
538
|
+
optional={"position"},
|
|
539
|
+
name="command",
|
|
540
|
+
)
|
|
541
|
+
normalized["path"] = _validate_path_or_reference(raw["path"], "command.path")
|
|
542
|
+
normalized["parent"] = _validate_path_or_reference(raw["parent"], "command.parent")
|
|
543
|
+
normalized["position"] = _validate_position(raw.get("position", {"mode": "append"}))
|
|
544
|
+
return normalized
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def validate_agent_batch(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
548
|
+
"""Validate a complete atomic batch envelope without touching a document."""
|
|
549
|
+
|
|
550
|
+
raw = dict(_require_mapping(value, "batch"))
|
|
551
|
+
required = {
|
|
552
|
+
"schemaVersion",
|
|
553
|
+
"input",
|
|
554
|
+
"output",
|
|
555
|
+
"commands",
|
|
556
|
+
"expectedRevision",
|
|
557
|
+
"idempotencyKey",
|
|
558
|
+
"dryRun",
|
|
559
|
+
"quality",
|
|
560
|
+
"verificationRequirements",
|
|
561
|
+
}
|
|
562
|
+
_require_exact_keys(raw, required=required, name="batch")
|
|
563
|
+
if raw["schemaVersion"] != AGENT_BATCH_SCHEMA:
|
|
564
|
+
raise AgentContractError("invalid_syntax", "unsupported batch schemaVersion", target="batch.schemaVersion")
|
|
565
|
+
|
|
566
|
+
input_ref = dict(_require_mapping(raw["input"], "batch.input"))
|
|
567
|
+
_require_exact_keys(input_ref, required={"filename"}, name="batch.input")
|
|
568
|
+
if not str(input_ref["filename"]).strip():
|
|
569
|
+
raise AgentContractError("invalid_syntax", "batch.input.filename is required", target="batch.input.filename")
|
|
570
|
+
|
|
571
|
+
output_ref = dict(_require_mapping(raw["output"], "batch.output"))
|
|
572
|
+
_require_exact_keys(output_ref, required={"filename", "overwrite"}, name="batch.output")
|
|
573
|
+
if not str(output_ref["filename"]).strip() or not isinstance(output_ref["overwrite"], bool):
|
|
574
|
+
raise AgentContractError("invalid_syntax", "batch.output is invalid", target="batch.output")
|
|
575
|
+
|
|
576
|
+
commands_value = raw["commands"]
|
|
577
|
+
if isinstance(commands_value, (str, bytes)) or not isinstance(commands_value, Sequence):
|
|
578
|
+
raise AgentContractError("invalid_syntax", "batch.commands must be an array", target="batch.commands")
|
|
579
|
+
if not commands_value or len(commands_value) > MAX_COMMANDS:
|
|
580
|
+
raise AgentContractError("resource_limit", "batch.commands count is out of bounds", target="batch.commands")
|
|
581
|
+
commands = [validate_agent_command(_require_mapping(item, "command")) for item in commands_value]
|
|
582
|
+
command_ids = [command["commandId"] for command in commands]
|
|
583
|
+
if len(command_ids) != len(set(command_ids)):
|
|
584
|
+
raise AgentContractError("invariant_violation", "batch commandId values must be unique", target="batch.commands")
|
|
585
|
+
for index, command in enumerate(commands):
|
|
586
|
+
for key in ("path", "parent"):
|
|
587
|
+
target = command.get(key)
|
|
588
|
+
match = COMMAND_REF_PATTERN.fullmatch(str(target or ""))
|
|
589
|
+
if not match:
|
|
590
|
+
continue
|
|
591
|
+
referenced = match.group(1)
|
|
592
|
+
if referenced not in command_ids[:index]:
|
|
593
|
+
raise AgentContractError(
|
|
594
|
+
"invalid_syntax",
|
|
595
|
+
f"command reference {referenced!r} must name an earlier command",
|
|
596
|
+
target=f"batch.commands[{index}].{key}",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
expected_revision = raw["expectedRevision"]
|
|
600
|
+
if expected_revision is not None and not REVISION_PATTERN.fullmatch(str(expected_revision)):
|
|
601
|
+
raise AgentContractError("invalid_syntax", "expectedRevision must be sha256 or null", target="batch.expectedRevision")
|
|
602
|
+
idempotency_key = raw["idempotencyKey"]
|
|
603
|
+
if idempotency_key is not None and not (1 <= len(str(idempotency_key)) <= 128):
|
|
604
|
+
raise AgentContractError("resource_limit", "idempotencyKey length is invalid", target="batch.idempotencyKey")
|
|
605
|
+
if not isinstance(raw["dryRun"], bool):
|
|
606
|
+
raise AgentContractError("invalid_syntax", "dryRun must be boolean", target="batch.dryRun")
|
|
607
|
+
|
|
608
|
+
requirements = raw["verificationRequirements"]
|
|
609
|
+
if isinstance(requirements, (str, bytes)) or not isinstance(requirements, Sequence):
|
|
610
|
+
raise AgentContractError(
|
|
611
|
+
"invalid_syntax", "verificationRequirements must be an array", target="batch.verificationRequirements"
|
|
612
|
+
)
|
|
613
|
+
unknown_requirements = sorted(set(requirements) - set(VERIFICATION_REQUIREMENTS))
|
|
614
|
+
if unknown_requirements:
|
|
615
|
+
raise AgentContractError(
|
|
616
|
+
"invalid_syntax",
|
|
617
|
+
f"unknown verification requirements: {unknown_requirements}",
|
|
618
|
+
target="batch.verificationRequirements",
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
"schemaVersion": AGENT_BATCH_SCHEMA,
|
|
623
|
+
"input": {"filename": str(input_ref["filename"])},
|
|
624
|
+
"output": {"filename": str(output_ref["filename"]), "overwrite": output_ref["overwrite"]},
|
|
625
|
+
"commands": commands,
|
|
626
|
+
"expectedRevision": None if expected_revision is None else str(expected_revision),
|
|
627
|
+
"idempotencyKey": None if idempotency_key is None else str(idempotency_key),
|
|
628
|
+
"dryRun": raw["dryRun"],
|
|
629
|
+
"quality": _validate_quality(raw["quality"]),
|
|
630
|
+
"verificationRequirements": list(dict.fromkeys(str(item) for item in requirements)),
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@dataclass(frozen=True, slots=True)
|
|
635
|
+
class AgentBatchResult:
|
|
636
|
+
ok: bool
|
|
637
|
+
rolled_back: bool
|
|
638
|
+
dry_run: bool
|
|
639
|
+
input_revision: str
|
|
640
|
+
document_revision: str
|
|
641
|
+
output_filename: str
|
|
642
|
+
command_results: tuple[Mapping[str, Any], ...] = ()
|
|
643
|
+
semantic_diff: Mapping[str, Any] = field(default_factory=dict)
|
|
644
|
+
verification_report: Mapping[str, Any] = field(default_factory=dict)
|
|
645
|
+
error: AgentError | None = None
|
|
646
|
+
|
|
647
|
+
def __post_init__(self) -> None:
|
|
648
|
+
for name, revision in (
|
|
649
|
+
("inputRevision", self.input_revision),
|
|
650
|
+
("documentRevision", self.document_revision),
|
|
651
|
+
):
|
|
652
|
+
if not REVISION_PATTERN.fullmatch(revision):
|
|
653
|
+
raise ValueError(f"{name} must be sha256")
|
|
654
|
+
if self.ok and self.rolled_back:
|
|
655
|
+
raise ValueError("successful batch cannot be rolled back")
|
|
656
|
+
if not self.ok and self.error is None:
|
|
657
|
+
raise ValueError("failed batch requires a structured error")
|
|
658
|
+
if len(self.command_results) > MAX_COMMANDS:
|
|
659
|
+
raise ValueError("too many command results")
|
|
660
|
+
_validate_json_value(self.command_results, name="result.commandResults")
|
|
661
|
+
_validate_json_value(self.semantic_diff, name="result.semanticDiff")
|
|
662
|
+
_validate_json_value(self.verification_report, name="result.verificationReport")
|
|
663
|
+
|
|
664
|
+
def to_dict(self) -> dict[str, Any]:
|
|
665
|
+
return {
|
|
666
|
+
"schemaVersion": AGENT_BATCH_RESULT_SCHEMA,
|
|
667
|
+
"ok": self.ok,
|
|
668
|
+
"rolledBack": self.rolled_back,
|
|
669
|
+
"dryRun": self.dry_run,
|
|
670
|
+
"inputRevision": self.input_revision,
|
|
671
|
+
"documentRevision": self.document_revision,
|
|
672
|
+
"output": {"filename": self.output_filename},
|
|
673
|
+
"commandResults": [dict(item) for item in self.command_results],
|
|
674
|
+
"semanticDiff": dict(self.semantic_diff),
|
|
675
|
+
"verificationReport": dict(self.verification_report),
|
|
676
|
+
"error": None if self.error is None else self.error.to_dict(),
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
def as_mutation_report(self) -> MutationReport:
|
|
680
|
+
"""Project this transaction result onto ``hwpx.mutation-report/v1`` (specs/032
|
|
681
|
+
§3). Additive — the fields above are untouched.
|
|
682
|
+
|
|
683
|
+
The agent write is a family-A rebuild (``document.to_bytes()``), so
|
|
684
|
+
``actualMode`` is ``"rebuild"`` and changed parts carry no ranges (a rebuilt
|
|
685
|
+
part is re-serialized whole). Preservation and verification are read from the
|
|
686
|
+
already-measured ``verification_report`` (``bytePreservation`` = the shared
|
|
687
|
+
``_member_diff``, ``openSafety``, ``realHancom``); layers that report never
|
|
688
|
+
measured — local ZIP records, an absent oracle — stay zero-verified /
|
|
689
|
+
``not_performed`` rather than being promoted to a pass.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
report = self.verification_report
|
|
693
|
+
byte = report.get("bytePreservation") or {}
|
|
694
|
+
changed_names = [
|
|
695
|
+
*byte.get("changedMembers", ()),
|
|
696
|
+
*byte.get("addedMembers", ()),
|
|
697
|
+
*byte.get("removedMembers", ()),
|
|
698
|
+
]
|
|
699
|
+
changed_parts = tuple(
|
|
700
|
+
ChangedPart(path=str(name), reason="dirty-part", ranges=None)
|
|
701
|
+
for name in changed_names
|
|
702
|
+
)
|
|
703
|
+
unchanged = byte.get("unchangedMemberCount")
|
|
704
|
+
preservation = PreservationSummary(
|
|
705
|
+
untouched_part_payloads=PreservationCounts(
|
|
706
|
+
verified=unchanged if isinstance(unchanged, int) else 0, changed=0
|
|
707
|
+
),
|
|
708
|
+
untouched_local_zip_records=PreservationCounts(verified=0, changed=0),
|
|
709
|
+
whole_package_identical=bool(byte) and not changed_names,
|
|
710
|
+
)
|
|
711
|
+
real_hancom = report.get("realHancom") or {}
|
|
712
|
+
verification = verification_from_open_safety(
|
|
713
|
+
report.get("openSafety"),
|
|
714
|
+
visual=visual_value_from_status(real_hancom.get("status")),
|
|
715
|
+
)
|
|
716
|
+
return MutationReport(
|
|
717
|
+
requested_mode="rebuild",
|
|
718
|
+
actual_mode="rebuild",
|
|
719
|
+
fallback_used=False,
|
|
720
|
+
changed_parts=changed_parts,
|
|
721
|
+
preservation=preservation,
|
|
722
|
+
verification=verification,
|
|
723
|
+
path=self.output_filename,
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def agent_contract_manifest() -> dict[str, Any]:
|
|
728
|
+
"""Return the deterministic, inspectable v1 contract manifest."""
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
"schemaVersion": AGENT_CATALOG_SCHEMA,
|
|
732
|
+
"schemas": {
|
|
733
|
+
"node": AGENT_NODE_SCHEMA,
|
|
734
|
+
"command": AGENT_COMMAND_SCHEMA,
|
|
735
|
+
"batch": AGENT_BATCH_SCHEMA,
|
|
736
|
+
"batchResult": AGENT_BATCH_RESULT_SCHEMA,
|
|
737
|
+
"error": AGENT_ERROR_SCHEMA,
|
|
738
|
+
},
|
|
739
|
+
"limits": {
|
|
740
|
+
"maxViewDepth": MAX_VIEW_DEPTH,
|
|
741
|
+
"maxChildrenPerNode": MAX_CHILDREN_PER_NODE,
|
|
742
|
+
"maxQueryResults": MAX_QUERY_RESULTS,
|
|
743
|
+
"maxTextChars": MAX_TEXT_CHARS,
|
|
744
|
+
"maxSelectorChars": MAX_SELECTOR_CHARS,
|
|
745
|
+
"maxCommands": MAX_COMMANDS,
|
|
746
|
+
"maxPropertiesPerCommand": MAX_PROPERTIES_PER_COMMAND,
|
|
747
|
+
},
|
|
748
|
+
"path": {
|
|
749
|
+
"root": "/",
|
|
750
|
+
"externalIndexBase": 1,
|
|
751
|
+
"stabilityLevels": list(STABILITY_LEVELS),
|
|
752
|
+
"commandReference": "$<commandId>.path",
|
|
753
|
+
},
|
|
754
|
+
"selector": {
|
|
755
|
+
"features": list(SELECTOR_FEATURES),
|
|
756
|
+
"kinds": list(SELECTOR_KINDS),
|
|
757
|
+
"attributes": list(SELECTOR_ATTRIBUTES),
|
|
758
|
+
"resultOrder": "document",
|
|
759
|
+
"xpath": False,
|
|
760
|
+
"regex": False,
|
|
761
|
+
"rawXml": False,
|
|
762
|
+
},
|
|
763
|
+
"operations": list(AGENT_OPERATIONS),
|
|
764
|
+
"nodeKinds": {
|
|
765
|
+
kind: {
|
|
766
|
+
"readableProperties": list(NODE_PROPERTY_CATALOG_V1[kind]["readable"]),
|
|
767
|
+
"editableProperties": list(NODE_PROPERTY_CATALOG_V1[kind]["editable"]),
|
|
768
|
+
"operations": list(NODE_PROPERTY_CATALOG_V1[kind]["operations"]),
|
|
769
|
+
}
|
|
770
|
+
for kind in NODE_KINDS
|
|
771
|
+
},
|
|
772
|
+
"verificationRequirements": list(VERIFICATION_REQUIREMENTS),
|
|
773
|
+
"qualityModes": list(QUALITY_MODES),
|
|
774
|
+
"errorCodes": list(ERROR_CODES),
|
|
775
|
+
"recoverability": list(RECOVERABILITY),
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
__all__ = [
|
|
780
|
+
"AGENT_BATCH_RESULT_SCHEMA",
|
|
781
|
+
"AGENT_BATCH_SCHEMA",
|
|
782
|
+
"AGENT_CATALOG_SCHEMA",
|
|
783
|
+
"AGENT_COMMAND_SCHEMA",
|
|
784
|
+
"AGENT_ERROR_SCHEMA",
|
|
785
|
+
"AGENT_NODE_SCHEMA",
|
|
786
|
+
"AGENT_OPERATIONS",
|
|
787
|
+
"AgentBatchResult",
|
|
788
|
+
"AgentContractError",
|
|
789
|
+
"AgentError",
|
|
790
|
+
"AgentNode",
|
|
791
|
+
"ERROR_CODES",
|
|
792
|
+
"MAX_CHILDREN_PER_NODE",
|
|
793
|
+
"MAX_COMMANDS",
|
|
794
|
+
"MAX_PROPERTIES_PER_COMMAND",
|
|
795
|
+
"MAX_QUERY_RESULTS",
|
|
796
|
+
"MAX_SELECTOR_CHARS",
|
|
797
|
+
"MAX_TEXT_CHARS",
|
|
798
|
+
"MAX_VIEW_DEPTH",
|
|
799
|
+
"NODE_KINDS",
|
|
800
|
+
"NODE_PROPERTY_CATALOG_V1",
|
|
801
|
+
"SELECTOR_ATTRIBUTES",
|
|
802
|
+
"SELECTOR_FEATURES",
|
|
803
|
+
"STABILITY_LEVELS",
|
|
804
|
+
"VERIFICATION_REQUIREMENTS",
|
|
805
|
+
"agent_contract_manifest",
|
|
806
|
+
"validate_agent_batch",
|
|
807
|
+
"validate_agent_command",
|
|
808
|
+
]
|