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,801 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Bounded semantic projection over one HWPX document revision."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
from collections import Counter, defaultdict
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from os import PathLike
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Sequence, cast
|
|
12
|
+
|
|
13
|
+
from hwpx.document import HwpxDocument
|
|
14
|
+
from hwpx.oxml import HwpxOxmlShape
|
|
15
|
+
|
|
16
|
+
from .model import (
|
|
17
|
+
MAX_CHILDREN_PER_NODE,
|
|
18
|
+
MAX_TEXT_CHARS,
|
|
19
|
+
MAX_VIEW_DEPTH,
|
|
20
|
+
AgentContractError,
|
|
21
|
+
AgentNode,
|
|
22
|
+
NODE_PROPERTY_CATALOG_V1,
|
|
23
|
+
)
|
|
24
|
+
from .path import SemanticPath, canonicalize_path, identified_segment, indexed_segment
|
|
25
|
+
from .query import QueryRecord as _QueryRecord
|
|
26
|
+
from .query import QueryResult, evaluate_selector, parse_selector
|
|
27
|
+
from .story import (
|
|
28
|
+
HeaderStoryBinding,
|
|
29
|
+
HeaderStoryPath,
|
|
30
|
+
parse_header_story_path,
|
|
31
|
+
resolve_header_story,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_HWP_UNITS_PER_MM = 7200 / 25.4
|
|
35
|
+
_SHAPE_KINDS = frozenset({"line", "rect", "ellipse", "arc", "polygon", "curve", "connectLine"})
|
|
36
|
+
_IGNORED_INLINE_KINDS = frozenset({"t", "tab", "lineBreak", "secPr", "fieldEnd"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _local_name(element: Any) -> str:
|
|
40
|
+
tag = getattr(element, "tag", "")
|
|
41
|
+
return str(tag).rsplit("}", 1)[-1]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _bounded_text(value: object, *, limit: int = 512) -> str:
|
|
45
|
+
limit = min(limit, MAX_TEXT_CHARS)
|
|
46
|
+
text = str(value or "")
|
|
47
|
+
return text if len(text) <= limit else text[: limit - 1] + "…"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _mm(value: object) -> float | None:
|
|
51
|
+
try:
|
|
52
|
+
units = int(str(value))
|
|
53
|
+
except (TypeError, ValueError):
|
|
54
|
+
return None
|
|
55
|
+
return round(units / _HWP_UNITS_PER_MM, 3)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _first_child(element: Any, local_name: str) -> Any | None:
|
|
59
|
+
for child in element:
|
|
60
|
+
if _local_name(child) == local_name:
|
|
61
|
+
return child
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _child_text(element: Any, local_name: str) -> str | None:
|
|
66
|
+
child = _first_child(element, local_name)
|
|
67
|
+
if child is None:
|
|
68
|
+
return None
|
|
69
|
+
value = "".join(str(part) for part in child.itertext()).strip()
|
|
70
|
+
return _bounded_text(value) if value else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _identity(element: Any, *names: str) -> str | None:
|
|
74
|
+
for name in names:
|
|
75
|
+
value = element.get(name)
|
|
76
|
+
if value is not None and str(value).strip():
|
|
77
|
+
return str(value).strip()
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(slots=True)
|
|
82
|
+
class NodeRecord:
|
|
83
|
+
"""Request-local native binding; never serialized directly."""
|
|
84
|
+
|
|
85
|
+
kind: str
|
|
86
|
+
path: str
|
|
87
|
+
stable_id: str | None
|
|
88
|
+
stability: str
|
|
89
|
+
summary: dict[str, Any]
|
|
90
|
+
native: Any
|
|
91
|
+
parent_path: str | None
|
|
92
|
+
attributes: dict[str, str] = field(default_factory=dict)
|
|
93
|
+
search_text: str = ""
|
|
94
|
+
child_paths: list[str] = field(default_factory=list)
|
|
95
|
+
unsupported_child_count: int = 0
|
|
96
|
+
unsupported_child_kinds: Counter[str] = field(default_factory=Counter)
|
|
97
|
+
|
|
98
|
+
def mark_unsupported(self, local_name: str, count: int = 1) -> None:
|
|
99
|
+
self.unsupported_child_kinds[local_name] += count
|
|
100
|
+
self.unsupported_child_count += count
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class HwpxAgentDocument:
|
|
104
|
+
"""A revision-bound semantic view with request-local native bindings."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, document: HwpxDocument, revision: str, *, owns_document: bool = False) -> None:
|
|
107
|
+
if not revision.startswith("sha256:") or len(revision) != 71:
|
|
108
|
+
raise AgentContractError("invalid_syntax", "revision must be sha256", target="revision")
|
|
109
|
+
self.document = document
|
|
110
|
+
self.revision = revision
|
|
111
|
+
self._owns_document = owns_document
|
|
112
|
+
self._records: dict[str, NodeRecord] = {}
|
|
113
|
+
self._ordered_paths: list[str] = []
|
|
114
|
+
self._ambiguous_paths: set[str] = set()
|
|
115
|
+
self._form_fields_by_paragraph: dict[Any, list[dict[str, Any]]] = defaultdict(list)
|
|
116
|
+
self._form_control_positions: set[tuple[int, int, int, int]] = set()
|
|
117
|
+
self._id_counts: Counter[tuple[str, str]] = Counter()
|
|
118
|
+
self._field_name_counts: Counter[str] = Counter()
|
|
119
|
+
self._collect_identities()
|
|
120
|
+
self._collect_form_fields()
|
|
121
|
+
self._project()
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def open(cls, source: str | PathLike[str] | bytes) -> "HwpxAgentDocument":
|
|
125
|
+
if isinstance(source, bytes):
|
|
126
|
+
data = source
|
|
127
|
+
else:
|
|
128
|
+
data = Path(source).read_bytes()
|
|
129
|
+
revision = "sha256:" + hashlib.sha256(data).hexdigest()
|
|
130
|
+
return cls(HwpxDocument.open(data), revision, owns_document=True)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_document(cls, document: HwpxDocument, *, revision: str) -> "HwpxAgentDocument":
|
|
134
|
+
return cls(document, revision, owns_document=False)
|
|
135
|
+
|
|
136
|
+
def __enter__(self) -> "HwpxAgentDocument":
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: # type: ignore[exit-return] # frozen public signature
|
|
140
|
+
self.close()
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def close(self) -> None:
|
|
144
|
+
if self._owns_document:
|
|
145
|
+
self.document.close()
|
|
146
|
+
self._owns_document = False
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def records(self) -> tuple[NodeRecord, ...]:
|
|
150
|
+
return tuple(self._records[path] for path in self._ordered_paths)
|
|
151
|
+
|
|
152
|
+
def _collect_identities(self) -> None:
|
|
153
|
+
tag_to_kind = {
|
|
154
|
+
"p": "paragraph",
|
|
155
|
+
"tbl": "table",
|
|
156
|
+
"pic": "picture",
|
|
157
|
+
"memo": "memo",
|
|
158
|
+
"footNote": "footnote",
|
|
159
|
+
"endNote": "endnote",
|
|
160
|
+
**{name: "shape" for name in _SHAPE_KINDS},
|
|
161
|
+
}
|
|
162
|
+
for section in self.document.sections:
|
|
163
|
+
for element in section.element.iter():
|
|
164
|
+
kind = tag_to_kind.get(_local_name(element))
|
|
165
|
+
if kind is None:
|
|
166
|
+
continue
|
|
167
|
+
value = _identity(element, "id", "instId", "instid")
|
|
168
|
+
if value:
|
|
169
|
+
self._id_counts[(kind, value)] += 1
|
|
170
|
+
|
|
171
|
+
def _collect_form_fields(self) -> None:
|
|
172
|
+
# This private iterator is deliberately consumed inside python-hwpx so
|
|
173
|
+
# later mutation can reuse its already-safe field matching semantics.
|
|
174
|
+
matches = self.document._iter_form_field_matches() # type: ignore[attr-defined]
|
|
175
|
+
for match in matches:
|
|
176
|
+
section_index = int(match["section_index"])
|
|
177
|
+
paragraph_index = int(match["paragraph_index_in_section"])
|
|
178
|
+
paragraph = match.get("_paragraph")
|
|
179
|
+
if paragraph is not None:
|
|
180
|
+
self._form_fields_by_paragraph[paragraph.element].append(match)
|
|
181
|
+
self._form_control_positions.add(
|
|
182
|
+
(
|
|
183
|
+
section_index,
|
|
184
|
+
paragraph_index,
|
|
185
|
+
int(match["run_index"]),
|
|
186
|
+
int(match["child_index"]),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
field_id = str(match.get("field_id") or match.get("id") or "").strip()
|
|
190
|
+
if field_id:
|
|
191
|
+
self._id_counts[("form-field", field_id)] += 1
|
|
192
|
+
name = str(match.get("name") or "").strip()
|
|
193
|
+
if name:
|
|
194
|
+
self._field_name_counts[name] += 1
|
|
195
|
+
|
|
196
|
+
def _add_record(self, record: NodeRecord) -> None:
|
|
197
|
+
if record.path in self._records:
|
|
198
|
+
raise AgentContractError(
|
|
199
|
+
"invariant_violation", f"duplicate canonical path: {record.path}", target=record.path
|
|
200
|
+
)
|
|
201
|
+
self._records[record.path] = record
|
|
202
|
+
self._ordered_paths.append(record.path)
|
|
203
|
+
if record.parent_path is not None:
|
|
204
|
+
self._records[record.parent_path].child_paths.append(record.path)
|
|
205
|
+
|
|
206
|
+
def _segment_for(
|
|
207
|
+
self,
|
|
208
|
+
*,
|
|
209
|
+
kind: str,
|
|
210
|
+
index: int,
|
|
211
|
+
identity: str | None,
|
|
212
|
+
name: str | None = None,
|
|
213
|
+
) -> tuple[Any, str | None, str]:
|
|
214
|
+
if identity:
|
|
215
|
+
candidate = identified_segment(kind, identity)
|
|
216
|
+
if self._id_counts[(kind, identity)] == 1:
|
|
217
|
+
return candidate, f"{kind}:{identity}", "native"
|
|
218
|
+
self._ambiguous_paths.add(SemanticPath((candidate,)).canonical)
|
|
219
|
+
return indexed_segment(kind, index), None, "positional"
|
|
220
|
+
if kind == "form-field" and name and self._field_name_counts[name] == 1:
|
|
221
|
+
return identified_segment(kind, name, attribute="name"), f"form-field-name:{name}", "derived"
|
|
222
|
+
if kind == "form-field" and name:
|
|
223
|
+
name_segment = identified_segment(kind, name, attribute="name")
|
|
224
|
+
self._ambiguous_paths.add(SemanticPath((name_segment,)).canonical)
|
|
225
|
+
return indexed_segment(kind, index), None, "positional"
|
|
226
|
+
|
|
227
|
+
def _register_ambiguous_relative(
|
|
228
|
+
self,
|
|
229
|
+
parent: SemanticPath,
|
|
230
|
+
kind: str,
|
|
231
|
+
identity: str,
|
|
232
|
+
*,
|
|
233
|
+
attribute: str = "id",
|
|
234
|
+
) -> None:
|
|
235
|
+
segment = identified_segment(kind, identity, attribute=attribute)
|
|
236
|
+
self._ambiguous_paths.add(parent.child(segment).canonical)
|
|
237
|
+
|
|
238
|
+
def _project(self) -> None:
|
|
239
|
+
root = NodeRecord(
|
|
240
|
+
kind="document",
|
|
241
|
+
path="/",
|
|
242
|
+
stable_id="document",
|
|
243
|
+
stability="derived",
|
|
244
|
+
summary={},
|
|
245
|
+
native=self.document,
|
|
246
|
+
parent_path=None,
|
|
247
|
+
attributes={"type": "document"},
|
|
248
|
+
)
|
|
249
|
+
self._add_record(root)
|
|
250
|
+
for section_index, section in enumerate(self.document.sections, start=1):
|
|
251
|
+
section_path = SemanticPath().child(indexed_segment("section", section_index))
|
|
252
|
+
page_size = section.properties.page_size
|
|
253
|
+
section_record = NodeRecord(
|
|
254
|
+
kind="section",
|
|
255
|
+
path=section_path.canonical,
|
|
256
|
+
stable_id=None,
|
|
257
|
+
stability="positional",
|
|
258
|
+
summary={
|
|
259
|
+
"index": section_index,
|
|
260
|
+
"partId": Path(section.part_name).name,
|
|
261
|
+
"paragraphCount": len(section.paragraphs),
|
|
262
|
+
"pageWidthMm": _mm(page_size.width),
|
|
263
|
+
"pageHeightMm": _mm(page_size.height),
|
|
264
|
+
},
|
|
265
|
+
native=section,
|
|
266
|
+
parent_path="/",
|
|
267
|
+
attributes={"type": "section"},
|
|
268
|
+
)
|
|
269
|
+
self._add_record(section_record)
|
|
270
|
+
for child in section.element:
|
|
271
|
+
local = _local_name(child)
|
|
272
|
+
if local not in {"p", "memogroup"}:
|
|
273
|
+
section_record.mark_unsupported(local)
|
|
274
|
+
for paragraph_index, paragraph in enumerate(section.paragraphs, start=1):
|
|
275
|
+
self._project_paragraph(
|
|
276
|
+
paragraph,
|
|
277
|
+
section_path,
|
|
278
|
+
paragraph_index,
|
|
279
|
+
section_index=section_index - 1,
|
|
280
|
+
paragraph_index_in_section=paragraph_index - 1,
|
|
281
|
+
)
|
|
282
|
+
for memo_index, memo in enumerate(section.memos, start=1):
|
|
283
|
+
identity = memo.id
|
|
284
|
+
segment, stable_id, stability = self._segment_for(
|
|
285
|
+
kind="memo", index=memo_index, identity=identity
|
|
286
|
+
)
|
|
287
|
+
if identity and stability == "positional":
|
|
288
|
+
self._register_ambiguous_relative(section_path, "memo", identity)
|
|
289
|
+
memo_path = section_path.child(segment)
|
|
290
|
+
self._add_record(
|
|
291
|
+
NodeRecord(
|
|
292
|
+
kind="memo",
|
|
293
|
+
path=memo_path.canonical,
|
|
294
|
+
stable_id=stable_id,
|
|
295
|
+
stability=stability,
|
|
296
|
+
summary={
|
|
297
|
+
"text": _bounded_text(memo.text),
|
|
298
|
+
"author": memo.attributes.get("author"),
|
|
299
|
+
},
|
|
300
|
+
native=memo,
|
|
301
|
+
parent_path=section_path.canonical,
|
|
302
|
+
attributes={"id": identity or "", "type": "memo"},
|
|
303
|
+
search_text=memo.text,
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
kinds = Counter(record.kind for record in self.records)
|
|
307
|
+
root.summary.update(
|
|
308
|
+
{
|
|
309
|
+
"sectionCount": kinds["section"],
|
|
310
|
+
"paragraphCount": kinds["paragraph"],
|
|
311
|
+
"tableCount": kinds["table"],
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _paragraph_summary(self, paragraph: Any) -> tuple[dict[str, Any], dict[str, str]]:
|
|
316
|
+
style_ref = paragraph.element.get("styleIDRef")
|
|
317
|
+
style = self.document.style(style_ref)
|
|
318
|
+
style_name = style.name if style is not None and style.name else style_ref
|
|
319
|
+
para_pr = self.document.paragraph_property(paragraph.element.get("paraPrIDRef"))
|
|
320
|
+
summary: dict[str, Any] = {"text": _bounded_text(paragraph.text), "style": style_name}
|
|
321
|
+
if para_pr is not None:
|
|
322
|
+
summary["alignment"] = para_pr.align.horizontal if para_pr.align is not None else None
|
|
323
|
+
summary["breakBefore"] = (
|
|
324
|
+
para_pr.break_setting.page_break_before if para_pr.break_setting is not None else None
|
|
325
|
+
)
|
|
326
|
+
summary["keepWithNext"] = (
|
|
327
|
+
para_pr.break_setting.keep_with_next if para_pr.break_setting is not None else None
|
|
328
|
+
)
|
|
329
|
+
summary["lineSpacingPercent"] = (
|
|
330
|
+
para_pr.line_spacing.value if para_pr.line_spacing is not None else None
|
|
331
|
+
)
|
|
332
|
+
attributes = {
|
|
333
|
+
"id": paragraph.element.get("id", ""),
|
|
334
|
+
"style": style_name or "",
|
|
335
|
+
"type": style.type if style is not None and style.type else "paragraph",
|
|
336
|
+
}
|
|
337
|
+
return summary, attributes
|
|
338
|
+
|
|
339
|
+
def _project_paragraph(
|
|
340
|
+
self,
|
|
341
|
+
paragraph: Any,
|
|
342
|
+
parent: SemanticPath,
|
|
343
|
+
index: int,
|
|
344
|
+
*,
|
|
345
|
+
section_index: int,
|
|
346
|
+
paragraph_index_in_section: int | None = None,
|
|
347
|
+
) -> None:
|
|
348
|
+
identity = _identity(paragraph.element, "id")
|
|
349
|
+
segment, stable_id, stability = self._segment_for(kind="paragraph", index=index, identity=identity)
|
|
350
|
+
if identity and stability == "positional":
|
|
351
|
+
self._register_ambiguous_relative(parent, "paragraph", identity)
|
|
352
|
+
path = parent.child(segment)
|
|
353
|
+
summary, attributes = self._paragraph_summary(paragraph)
|
|
354
|
+
record = NodeRecord(
|
|
355
|
+
kind="paragraph",
|
|
356
|
+
path=path.canonical,
|
|
357
|
+
stable_id=stable_id,
|
|
358
|
+
stability=stability,
|
|
359
|
+
summary=summary,
|
|
360
|
+
native=paragraph,
|
|
361
|
+
parent_path=parent.canonical,
|
|
362
|
+
attributes=attributes,
|
|
363
|
+
search_text=paragraph.text,
|
|
364
|
+
)
|
|
365
|
+
self._add_record(record)
|
|
366
|
+
for child in paragraph.element:
|
|
367
|
+
local = _local_name(child)
|
|
368
|
+
if local not in {"run", "lineSegArray", "linesegarray"}:
|
|
369
|
+
record.mark_unsupported(local)
|
|
370
|
+
fields_by_position: dict[tuple[int, int], list[dict[str, Any]]] = defaultdict(list)
|
|
371
|
+
for match in self._form_fields_by_paragraph[paragraph.element]:
|
|
372
|
+
fields_by_position[(int(match["run_index"]), int(match["child_index"]))].append(match)
|
|
373
|
+
|
|
374
|
+
object_indexes: Counter[str] = Counter()
|
|
375
|
+
for run_index, run in enumerate(paragraph.runs, start=1):
|
|
376
|
+
run_path = path.child(indexed_segment("run", run_index))
|
|
377
|
+
style = run.style
|
|
378
|
+
child_attrs = style.child_attributes if style is not None else {}
|
|
379
|
+
run_summary = {
|
|
380
|
+
"text": _bounded_text(run.text),
|
|
381
|
+
"bold": "bold" in child_attrs,
|
|
382
|
+
"italic": "italic" in child_attrs,
|
|
383
|
+
"underline": child_attrs.get("underline", {}).get("type", "NONE").upper() != "NONE",
|
|
384
|
+
"fontSizePt": (
|
|
385
|
+
round(int(style.attributes["height"]) / 100, 2)
|
|
386
|
+
if style is not None and str(style.attributes.get("height", "")).isdigit()
|
|
387
|
+
else None
|
|
388
|
+
),
|
|
389
|
+
"color": style.text_color() if style is not None else None,
|
|
390
|
+
}
|
|
391
|
+
self._add_record(
|
|
392
|
+
NodeRecord(
|
|
393
|
+
kind="run",
|
|
394
|
+
path=run_path.canonical,
|
|
395
|
+
stable_id=None,
|
|
396
|
+
stability="positional",
|
|
397
|
+
summary=run_summary,
|
|
398
|
+
native=run,
|
|
399
|
+
parent_path=path.canonical,
|
|
400
|
+
attributes={"type": "run"},
|
|
401
|
+
search_text=run.text,
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
for child_index, child in enumerate(run.element):
|
|
405
|
+
local = _local_name(child)
|
|
406
|
+
if local == "tbl":
|
|
407
|
+
object_indexes["table"] += 1
|
|
408
|
+
self._project_table(paragraph, child, path, object_indexes["table"])
|
|
409
|
+
elif local == "pic":
|
|
410
|
+
object_indexes["picture"] += 1
|
|
411
|
+
self._project_picture(child, paragraph, path, object_indexes["picture"])
|
|
412
|
+
elif local in _SHAPE_KINDS:
|
|
413
|
+
object_indexes["shape"] += 1
|
|
414
|
+
self._project_shape(child, paragraph, path, object_indexes["shape"])
|
|
415
|
+
elif local in {"footNote", "endNote"}:
|
|
416
|
+
kind = "footnote" if local == "footNote" else "endnote"
|
|
417
|
+
object_indexes[kind] += 1
|
|
418
|
+
notes = paragraph.footnotes if kind == "footnote" else paragraph.endnotes
|
|
419
|
+
note = next(candidate for candidate in notes if candidate.element is child)
|
|
420
|
+
self._project_note(note, path, kind, object_indexes[kind])
|
|
421
|
+
elif local == "ctrl":
|
|
422
|
+
matches = fields_by_position.get((run_index - 1, child_index), [])
|
|
423
|
+
if matches:
|
|
424
|
+
for match in matches:
|
|
425
|
+
object_indexes["form-field"] += 1
|
|
426
|
+
self._project_form_field(match, path, object_indexes["form-field"])
|
|
427
|
+
elif len(child) > 0 and all(
|
|
428
|
+
_local_name(control_child) in {"fieldEnd", "colPr", "secPr"}
|
|
429
|
+
for control_child in child
|
|
430
|
+
):
|
|
431
|
+
# A matched FORM field is projected from its fieldBegin. Its paired
|
|
432
|
+
# fieldEnd and section/column controls carry no additional subtree
|
|
433
|
+
# semantics and must not make an otherwise complete block unsupported.
|
|
434
|
+
continue
|
|
435
|
+
else:
|
|
436
|
+
record.mark_unsupported("ctrl")
|
|
437
|
+
elif local not in _IGNORED_INLINE_KINDS:
|
|
438
|
+
record.mark_unsupported(local)
|
|
439
|
+
|
|
440
|
+
def _project_table(self, paragraph: Any, element: Any, parent: SemanticPath, index: int) -> None:
|
|
441
|
+
table = next(candidate for candidate in paragraph.tables if candidate.element is element)
|
|
442
|
+
identity = _identity(element, "id")
|
|
443
|
+
segment, stable_id, stability = self._segment_for(kind="table", index=index, identity=identity)
|
|
444
|
+
if identity and stability == "positional":
|
|
445
|
+
self._register_ambiguous_relative(parent, "table", identity)
|
|
446
|
+
path = parent.child(segment)
|
|
447
|
+
size = _first_child(element, "sz")
|
|
448
|
+
pos = _first_child(element, "pos")
|
|
449
|
+
table_record = NodeRecord(
|
|
450
|
+
kind="table",
|
|
451
|
+
path=path.canonical,
|
|
452
|
+
stable_id=stable_id,
|
|
453
|
+
stability=stability,
|
|
454
|
+
summary={
|
|
455
|
+
"rowCount": table.row_count,
|
|
456
|
+
"columnCount": table.column_count,
|
|
457
|
+
"caption": _child_text(element, "caption"),
|
|
458
|
+
"widthMm": _mm(size.get("width")) if size is not None else None,
|
|
459
|
+
"alignment": pos.get("horzAlign") if pos is not None else None,
|
|
460
|
+
},
|
|
461
|
+
native=table,
|
|
462
|
+
parent_path=parent.canonical,
|
|
463
|
+
attributes={"id": identity or "", "type": element.get("numberingType", "table")},
|
|
464
|
+
search_text=paragraph.text,
|
|
465
|
+
)
|
|
466
|
+
self._add_record(table_record)
|
|
467
|
+
for child in element:
|
|
468
|
+
local = _local_name(child)
|
|
469
|
+
if local not in {"sz", "pos", "caption", "outMargin", "inMargin", "tr"}:
|
|
470
|
+
table_record.mark_unsupported(local)
|
|
471
|
+
for row_index, row in enumerate(table.rows, start=1):
|
|
472
|
+
row_path = path.child(indexed_segment("row", row_index))
|
|
473
|
+
heights = [cell.height for cell in row.cells]
|
|
474
|
+
self._add_record(
|
|
475
|
+
NodeRecord(
|
|
476
|
+
kind="row",
|
|
477
|
+
path=row_path.canonical,
|
|
478
|
+
stable_id=None,
|
|
479
|
+
stability="positional",
|
|
480
|
+
summary={
|
|
481
|
+
"index": row_index,
|
|
482
|
+
"cellCount": len(row.cells),
|
|
483
|
+
"heightMm": _mm(max(heights)) if heights else None,
|
|
484
|
+
},
|
|
485
|
+
native=row,
|
|
486
|
+
parent_path=path.canonical,
|
|
487
|
+
attributes={"type": "row"},
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
for cell_index, cell in enumerate(row.cells, start=1):
|
|
491
|
+
cell_path = row_path.child(indexed_segment("cell", cell_index))
|
|
492
|
+
address_row, address_column = cell.address
|
|
493
|
+
row_span, column_span = cell.span
|
|
494
|
+
sublist = _first_child(cell.element, "subList")
|
|
495
|
+
cell_record = NodeRecord(
|
|
496
|
+
kind="cell",
|
|
497
|
+
path=cell_path.canonical,
|
|
498
|
+
stable_id=None,
|
|
499
|
+
stability="positional",
|
|
500
|
+
summary={
|
|
501
|
+
"text": _bounded_text(cell.text),
|
|
502
|
+
"row": address_row + 1,
|
|
503
|
+
"column": address_column + 1,
|
|
504
|
+
"rowSpan": row_span,
|
|
505
|
+
"columnSpan": column_span,
|
|
506
|
+
"verticalAlignment": (
|
|
507
|
+
sublist.get("vertAlign") if sublist is not None else None
|
|
508
|
+
),
|
|
509
|
+
},
|
|
510
|
+
native=cell,
|
|
511
|
+
parent_path=row_path.canonical,
|
|
512
|
+
attributes={"type": "cell"},
|
|
513
|
+
search_text=cell.text,
|
|
514
|
+
)
|
|
515
|
+
self._add_record(cell_record)
|
|
516
|
+
for child in cell.element:
|
|
517
|
+
local = _local_name(child)
|
|
518
|
+
if local not in {
|
|
519
|
+
"subList",
|
|
520
|
+
"cellAddr",
|
|
521
|
+
"cellSpan",
|
|
522
|
+
"cellSz",
|
|
523
|
+
"cellMargin",
|
|
524
|
+
}:
|
|
525
|
+
cell_record.mark_unsupported(local)
|
|
526
|
+
for paragraph_index, nested in enumerate(cell.paragraphs, start=1):
|
|
527
|
+
self._project_paragraph(
|
|
528
|
+
nested,
|
|
529
|
+
cell_path,
|
|
530
|
+
paragraph_index,
|
|
531
|
+
section_index=-1,
|
|
532
|
+
paragraph_index_in_section=None,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def _project_picture(
|
|
536
|
+
self, element: Any, paragraph: Any, parent: SemanticPath, index: int
|
|
537
|
+
) -> None:
|
|
538
|
+
identity = _identity(element, "id", "instid", "instId")
|
|
539
|
+
segment, stable_id, stability = self._segment_for(kind="picture", index=index, identity=identity)
|
|
540
|
+
if identity and stability == "positional":
|
|
541
|
+
self._register_ambiguous_relative(parent, "picture", identity)
|
|
542
|
+
path = parent.child(segment)
|
|
543
|
+
image = next((child for child in element.iter() if _local_name(child) == "img"), None)
|
|
544
|
+
size = _first_child(element, "sz")
|
|
545
|
+
name = element.get("name") or (image.get("binaryItemIDRef") if image is not None else None)
|
|
546
|
+
self._add_record(
|
|
547
|
+
NodeRecord(
|
|
548
|
+
kind="picture",
|
|
549
|
+
path=path.canonical,
|
|
550
|
+
stable_id=stable_id,
|
|
551
|
+
stability=stability,
|
|
552
|
+
summary={
|
|
553
|
+
"name": name,
|
|
554
|
+
"altText": (
|
|
555
|
+
_child_text(element, "shapeComment")
|
|
556
|
+
or element.get("altText")
|
|
557
|
+
or element.get("title")
|
|
558
|
+
),
|
|
559
|
+
"widthMm": _mm(size.get("width")) if size is not None else None,
|
|
560
|
+
"heightMm": _mm(size.get("height")) if size is not None else None,
|
|
561
|
+
},
|
|
562
|
+
native=element,
|
|
563
|
+
parent_path=parent.canonical,
|
|
564
|
+
attributes={"id": identity or "", "name": name or "", "type": "picture"},
|
|
565
|
+
search_text=name or "",
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def _project_shape(
|
|
570
|
+
self, element: Any, paragraph: Any, parent: SemanticPath, index: int
|
|
571
|
+
) -> None:
|
|
572
|
+
shape = HwpxOxmlShape(element, paragraph)
|
|
573
|
+
identity = shape.inst_id
|
|
574
|
+
segment, stable_id, stability = self._segment_for(kind="shape", index=index, identity=identity)
|
|
575
|
+
if identity and stability == "positional":
|
|
576
|
+
self._register_ambiguous_relative(parent, "shape", identity)
|
|
577
|
+
path = parent.child(segment)
|
|
578
|
+
pos = _first_child(element, "pos")
|
|
579
|
+
name = element.get("name")
|
|
580
|
+
self._add_record(
|
|
581
|
+
NodeRecord(
|
|
582
|
+
kind="shape",
|
|
583
|
+
path=path.canonical,
|
|
584
|
+
stable_id=stable_id,
|
|
585
|
+
stability=stability,
|
|
586
|
+
summary={
|
|
587
|
+
"shapeType": shape.shape_type,
|
|
588
|
+
"name": name,
|
|
589
|
+
"altText": (
|
|
590
|
+
_child_text(element, "shapeComment")
|
|
591
|
+
or element.get("altText")
|
|
592
|
+
or element.get("title")
|
|
593
|
+
),
|
|
594
|
+
"xMm": _mm(pos.get("horzOffset")) if pos is not None else None,
|
|
595
|
+
"yMm": _mm(pos.get("vertOffset")) if pos is not None else None,
|
|
596
|
+
"widthMm": _mm(shape.width),
|
|
597
|
+
"heightMm": _mm(shape.height),
|
|
598
|
+
},
|
|
599
|
+
native=shape,
|
|
600
|
+
parent_path=parent.canonical,
|
|
601
|
+
attributes={"id": identity or "", "name": name or "", "type": shape.shape_type},
|
|
602
|
+
search_text=name or "",
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
def _project_note(self, note: Any, parent: SemanticPath, kind: str, index: int) -> None:
|
|
607
|
+
identity = note.inst_id
|
|
608
|
+
segment, stable_id, stability = self._segment_for(kind=kind, index=index, identity=identity)
|
|
609
|
+
if identity and stability == "positional":
|
|
610
|
+
self._register_ambiguous_relative(parent, kind, identity)
|
|
611
|
+
path = parent.child(segment)
|
|
612
|
+
self._add_record(
|
|
613
|
+
NodeRecord(
|
|
614
|
+
kind=kind,
|
|
615
|
+
path=path.canonical,
|
|
616
|
+
stable_id=stable_id,
|
|
617
|
+
stability=stability,
|
|
618
|
+
summary={"text": _bounded_text(note.text)},
|
|
619
|
+
native=note,
|
|
620
|
+
parent_path=parent.canonical,
|
|
621
|
+
attributes={"id": identity or "", "type": kind},
|
|
622
|
+
search_text=note.text,
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def _project_form_field(
|
|
627
|
+
self, match: dict[str, Any], parent: SemanticPath, index: int
|
|
628
|
+
) -> None:
|
|
629
|
+
identity = str(match.get("field_id") or match.get("id") or "").strip() or None
|
|
630
|
+
name = str(match.get("name") or "").strip() or None
|
|
631
|
+
segment, stable_id, stability = self._segment_for(
|
|
632
|
+
kind="form-field", index=index, identity=identity, name=name
|
|
633
|
+
)
|
|
634
|
+
if identity and stability == "positional":
|
|
635
|
+
self._register_ambiguous_relative(parent, "form-field", identity)
|
|
636
|
+
elif name and stability == "positional":
|
|
637
|
+
self._register_ambiguous_relative(parent, "form-field", name, attribute="name")
|
|
638
|
+
path = parent.child(segment)
|
|
639
|
+
runs = match.get("_runs") or []
|
|
640
|
+
ctrl = None
|
|
641
|
+
begin = None
|
|
642
|
+
try:
|
|
643
|
+
ctrl = list(runs[int(match["run_index"])])[int(match["child_index"])]
|
|
644
|
+
begin = next(child for child in ctrl if _local_name(child) == "fieldBegin")
|
|
645
|
+
except (IndexError, KeyError, StopIteration, TypeError, ValueError):
|
|
646
|
+
pass
|
|
647
|
+
read_only = None
|
|
648
|
+
if begin is not None:
|
|
649
|
+
editable = begin.get("editable")
|
|
650
|
+
if editable is not None:
|
|
651
|
+
read_only = str(editable).strip().lower() in {"0", "false", "no"}
|
|
652
|
+
self._add_record(
|
|
653
|
+
NodeRecord(
|
|
654
|
+
kind="form-field",
|
|
655
|
+
path=path.canonical,
|
|
656
|
+
stable_id=stable_id,
|
|
657
|
+
stability=stability,
|
|
658
|
+
summary={
|
|
659
|
+
"name": name,
|
|
660
|
+
"value": _bounded_text(match.get("current_value")),
|
|
661
|
+
"fieldType": match.get("field_type"),
|
|
662
|
+
"readOnly": read_only,
|
|
663
|
+
},
|
|
664
|
+
native=match,
|
|
665
|
+
parent_path=parent.canonical,
|
|
666
|
+
attributes={
|
|
667
|
+
"id": identity or "",
|
|
668
|
+
"name": name or "",
|
|
669
|
+
"type": str(
|
|
670
|
+
match.get("field_type") or match.get("control_type") or "form-field"
|
|
671
|
+
),
|
|
672
|
+
},
|
|
673
|
+
search_text=" ".join(
|
|
674
|
+
str(match.get(key) or "")
|
|
675
|
+
for key in ("name", "current_value", "prompt", "instruction")
|
|
676
|
+
),
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def resolve_record(
|
|
681
|
+
self,
|
|
682
|
+
path: str,
|
|
683
|
+
*,
|
|
684
|
+
expected_revision: str | None = None,
|
|
685
|
+
require_stable: bool = False,
|
|
686
|
+
) -> NodeRecord:
|
|
687
|
+
if expected_revision is not None and expected_revision != self.revision:
|
|
688
|
+
raise AgentContractError("stale_revision", "document revision does not match", target=path)
|
|
689
|
+
canonical = canonicalize_path(path)
|
|
690
|
+
if canonical in self._ambiguous_paths:
|
|
691
|
+
raise AgentContractError("ambiguous_target", "path identity is duplicated", target=canonical)
|
|
692
|
+
record = self._records.get(canonical)
|
|
693
|
+
if record is None:
|
|
694
|
+
raise AgentContractError("not_found", f"semantic path not found: {canonical}", target=canonical)
|
|
695
|
+
if require_stable and record.stability == "positional":
|
|
696
|
+
raise AgentContractError("volatile_target", "target has a positional path", target=canonical)
|
|
697
|
+
return record
|
|
698
|
+
|
|
699
|
+
def _resolve_header_story(
|
|
700
|
+
self,
|
|
701
|
+
path: str | HeaderStoryPath,
|
|
702
|
+
*,
|
|
703
|
+
expected_revision: str | None = None,
|
|
704
|
+
) -> HeaderStoryBinding:
|
|
705
|
+
"""Resolve the private command-only existing-header seam.
|
|
706
|
+
|
|
707
|
+
Header stories deliberately remain absent from ``records``, ``get``,
|
|
708
|
+
and ``query`` so the frozen public projection/catalog does not drift.
|
|
709
|
+
"""
|
|
710
|
+
|
|
711
|
+
parsed = parse_header_story_path(path) if isinstance(path, str) else path
|
|
712
|
+
if expected_revision is not None and expected_revision != self.revision:
|
|
713
|
+
raise AgentContractError(
|
|
714
|
+
"stale_revision", "document revision does not match", target=parsed.canonical
|
|
715
|
+
)
|
|
716
|
+
return resolve_header_story(self.document, parsed)
|
|
717
|
+
|
|
718
|
+
def _public_node(self, record: NodeRecord, *, depth: int, child_limit: int) -> AgentNode:
|
|
719
|
+
supported_total = len(record.child_paths)
|
|
720
|
+
selected_paths = record.child_paths[:child_limit] if depth > 0 else []
|
|
721
|
+
children = tuple(
|
|
722
|
+
self._public_node(self._records[path], depth=depth - 1, child_limit=child_limit)
|
|
723
|
+
for path in selected_paths
|
|
724
|
+
)
|
|
725
|
+
truncated = supported_total - len(selected_paths)
|
|
726
|
+
catalog = NODE_PROPERTY_CATALOG_V1[record.kind]
|
|
727
|
+
return AgentNode(
|
|
728
|
+
kind=record.kind,
|
|
729
|
+
path=record.path,
|
|
730
|
+
stable_id=record.stable_id,
|
|
731
|
+
stability=record.stability,
|
|
732
|
+
summary=record.summary,
|
|
733
|
+
child_count=supported_total + record.unsupported_child_count,
|
|
734
|
+
children=children,
|
|
735
|
+
unsupported_child_count=record.unsupported_child_count,
|
|
736
|
+
truncated_child_count=truncated,
|
|
737
|
+
readable_properties=tuple(catalog["readable"]),
|
|
738
|
+
editable_properties=tuple(catalog["editable"]),
|
|
739
|
+
operations=tuple(catalog["operations"]),
|
|
740
|
+
revision=self.revision,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
def get(
|
|
744
|
+
self,
|
|
745
|
+
path: str = "/",
|
|
746
|
+
*,
|
|
747
|
+
depth: int = 1,
|
|
748
|
+
child_limit: int = 50,
|
|
749
|
+
expected_revision: str | None = None,
|
|
750
|
+
) -> AgentNode:
|
|
751
|
+
if isinstance(depth, bool) or not isinstance(depth, int) or not 0 <= depth <= MAX_VIEW_DEPTH:
|
|
752
|
+
raise AgentContractError(
|
|
753
|
+
"resource_limit", f"depth must be 0..{MAX_VIEW_DEPTH}", target="depth"
|
|
754
|
+
)
|
|
755
|
+
if (
|
|
756
|
+
isinstance(child_limit, bool)
|
|
757
|
+
or not isinstance(child_limit, int)
|
|
758
|
+
or not 1 <= child_limit <= MAX_CHILDREN_PER_NODE
|
|
759
|
+
):
|
|
760
|
+
raise AgentContractError(
|
|
761
|
+
"resource_limit",
|
|
762
|
+
f"childLimit must be 1..{MAX_CHILDREN_PER_NODE}",
|
|
763
|
+
target="childLimit",
|
|
764
|
+
)
|
|
765
|
+
record = self.resolve_record(path, expected_revision=expected_revision)
|
|
766
|
+
return self._public_node(record, depth=depth, child_limit=child_limit)
|
|
767
|
+
|
|
768
|
+
def query(
|
|
769
|
+
self,
|
|
770
|
+
selector: str,
|
|
771
|
+
*,
|
|
772
|
+
limit: int,
|
|
773
|
+
node_depth: int = 0,
|
|
774
|
+
child_limit: int = 20,
|
|
775
|
+
expected_revision: str | None = None,
|
|
776
|
+
) -> QueryResult:
|
|
777
|
+
if expected_revision is not None and expected_revision != self.revision:
|
|
778
|
+
raise AgentContractError("stale_revision", "document revision does not match", target="selector")
|
|
779
|
+
parsed = parse_selector(selector)
|
|
780
|
+
query_records = cast(Sequence[_QueryRecord], self.records)
|
|
781
|
+
matches, truncated = evaluate_selector(query_records, parsed, limit=limit)
|
|
782
|
+
if not 0 <= node_depth <= MAX_VIEW_DEPTH:
|
|
783
|
+
raise AgentContractError("resource_limit", "nodeDepth is out of bounds", target="nodeDepth")
|
|
784
|
+
if not 1 <= child_limit <= MAX_CHILDREN_PER_NODE:
|
|
785
|
+
raise AgentContractError("resource_limit", "childLimit is out of bounds", target="childLimit")
|
|
786
|
+
return QueryResult(
|
|
787
|
+
selector=selector,
|
|
788
|
+
revision=self.revision,
|
|
789
|
+
nodes=tuple(
|
|
790
|
+
self._public_node(
|
|
791
|
+
cast(NodeRecord, record),
|
|
792
|
+
depth=node_depth,
|
|
793
|
+
child_limit=child_limit,
|
|
794
|
+
)
|
|
795
|
+
for record in matches
|
|
796
|
+
),
|
|
797
|
+
truncated=truncated,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
__all__ = ["HwpxAgentDocument", "NodeRecord"]
|