ai-editor-tree-engine 1.0.147__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. ai_editor_tree_engine-1.0.147.dist-info/METADATA +19 -0
  2. ai_editor_tree_engine-1.0.147.dist-info/RECORD +72 -0
  3. ai_editor_tree_engine-1.0.147.dist-info/WHEEL +5 -0
  4. ai_editor_tree_engine-1.0.147.dist-info/licenses/LICENSE +21 -0
  5. ai_editor_tree_engine-1.0.147.dist-info/top_level.txt +1 -0
  6. tree_engine/core/address.py +376 -0
  7. tree_engine/core/identifier_map.py +266 -0
  8. tree_engine/core/identity.py +226 -0
  9. tree_engine/core/instrumentation.py +399 -0
  10. tree_engine/core/integrity.py +341 -0
  11. tree_engine/core/integrity_full.py +382 -0
  12. tree_engine/core/journal.py +260 -0
  13. tree_engine/core/journal_compose.py +568 -0
  14. tree_engine/core/live_tree.py +672 -0
  15. tree_engine/core/locking.py +188 -0
  16. tree_engine/core/move.py +518 -0
  17. tree_engine/core/move_references.py +323 -0
  18. tree_engine/core/node_types.py +408 -0
  19. tree_engine/core/nodes.py +348 -0
  20. tree_engine/core/operations.py +609 -0
  21. tree_engine/core/plugin_boundary.py +160 -0
  22. tree_engine/core/position_map.py +263 -0
  23. tree_engine/core/reference_cache.py +360 -0
  24. tree_engine/core/references.py +223 -0
  25. tree_engine/core/reparse.py +378 -0
  26. tree_engine/core/short_id.py +296 -0
  27. tree_engine/core/subtree_apply.py +397 -0
  28. tree_engine/core/subtree_copy.py +399 -0
  29. tree_engine/core/transactions.py +397 -0
  30. tree_engine/core/trivia.py +396 -0
  31. tree_engine/core/updates.py +398 -0
  32. tree_engine/core/validation.py +377 -0
  33. tree_engine/errors.py +95 -0
  34. tree_engine/exceptions.py +326 -0
  35. tree_engine/facade.py +2723 -0
  36. tree_engine/plugins/bsl/dialects.py +303 -0
  37. tree_engine/plugins/bsl/generator.py +401 -0
  38. tree_engine/plugins/bsl/import_map.py +371 -0
  39. tree_engine/plugins/bsl/plugin.py +366 -0
  40. tree_engine/plugins/contract.py +351 -0
  41. tree_engine/plugins/detachable.py +123 -0
  42. tree_engine/plugins/fallback.py +421 -0
  43. tree_engine/plugins/ini_format.py +317 -0
  44. tree_engine/plugins/json_format.py +789 -0
  45. tree_engine/plugins/json_pointer.py +203 -0
  46. tree_engine/plugins/plain_text.py +1182 -0
  47. tree_engine/plugins/python/export_map.py +306 -0
  48. tree_engine/plugins/python/import_map.py +357 -0
  49. tree_engine/plugins/python/plugin.py +695 -0
  50. tree_engine/plugins/registry.py +399 -0
  51. tree_engine/plugins/selection.py +228 -0
  52. tree_engine/plugins/toml_format.py +549 -0
  53. tree_engine/plugins/yaml/builder.py +49 -0
  54. tree_engine/plugins/yaml/emitter.py +190 -0
  55. tree_engine/plugins/yaml/flow.py +196 -0
  56. tree_engine/plugins/yaml/plugin.py +317 -0
  57. tree_engine/plugins/yaml/reader.py +359 -0
  58. tree_engine/plugins/yaml/scanner.py +289 -0
  59. tree_engine/query/adapter.py +265 -0
  60. tree_engine/query/engine.py +424 -0
  61. tree_engine/query/inspection.py +498 -0
  62. tree_engine/query/outline.py +288 -0
  63. tree_engine/query/predicates.py +236 -0
  64. tree_engine/query/results.py +160 -0
  65. tree_engine/query/selector.py +400 -0
  66. tree_engine/storage/codec.py +400 -0
  67. tree_engine/storage/document_bridge.py +473 -0
  68. tree_engine/storage/file_txn.py +351 -0
  69. tree_engine/storage/history.py +106 -0
  70. tree_engine/storage/lifecycle.py +572 -0
  71. tree_engine/storage/schema.py +403 -0
  72. tree_engine/storage/session_guard.py +398 -0
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-editor-tree-engine
3
+ Version: 1.0.147
4
+ Summary: Format-independent parse/tree/identity/render engine used as the ai-editor server's internals
5
+ Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ License-File: LICENSE
9
+ Requires-Dist: lark<2.0,>=1.3.1
10
+ Provides-Extra: tree-engine-python
11
+ Requires-Dist: libcst<2.0,>=1.8.6; extra == "tree-engine-python"
12
+ Provides-Extra: tree-engine-bsl
13
+ Requires-Dist: tree-sitter<0.26,>=0.25; extra == "tree-engine-bsl"
14
+ Requires-Dist: tree-sitter-bsl<0.2,>=0.1.6; extra == "tree-engine-bsl"
15
+ Provides-Extra: tree-engine
16
+ Requires-Dist: libcst<2.0,>=1.8.6; extra == "tree-engine"
17
+ Requires-Dist: tree-sitter<0.26,>=0.25; extra == "tree-engine"
18
+ Requires-Dist: tree-sitter-bsl<0.2,>=0.1.6; extra == "tree-engine"
19
+ Dynamic: license-file
@@ -0,0 +1,72 @@
1
+ ai_editor_tree_engine-1.0.147.dist-info/licenses/LICENSE,sha256=6KdtUcTwmTRbJrAmYjVn7e6S-V42ubeDJ-AiVEzZ510,1075
2
+ tree_engine/errors.py,sha256=m07NG827Xtso7165eF9nTxV9ignaZ5aJfvqvV2Jm0sQ,3881
3
+ tree_engine/exceptions.py,sha256=mHoPsrQfM_RY_QDkt1lFtgPy87bpT7BQizNn-Wmqv1E,10640
4
+ tree_engine/facade.py,sha256=CYf43_bRPKd98E9C0HNT1vWzojXX7fRKSdOrgJ1o4-8,133214
5
+ tree_engine/core/address.py,sha256=aGeiIZAVtVCjpgPdOY7pE9-jkbQNUWH1DsddSHFSO_E,14741
6
+ tree_engine/core/identifier_map.py,sha256=OXK_2r8ZZ3nohJqUfwNwntJjjHmxMdC6ZPscjpOfw_I,11681
7
+ tree_engine/core/identity.py,sha256=BcUMWoPpvBC-8h81G_RQkCd3Wy8ymFI91U0ppk0ZC4A,8157
8
+ tree_engine/core/instrumentation.py,sha256=p_4jwEtgWH25f59N6xKND2VdNGGAT3VqcD6h473Z8e8,17121
9
+ tree_engine/core/integrity.py,sha256=sXl-SyfsUo7XiJG1lU45J3NK3hD2exHBy2s0WMLzAAo,14560
10
+ tree_engine/core/integrity_full.py,sha256=dyoMc2vvTMcU5grGBWy8z7Zf6fpXexhuviI0paSpeQk,16272
11
+ tree_engine/core/journal.py,sha256=xjA7WlWdOEqKsMSi4bvwJMvkGpzH2NJ_HYP-M8vGDz8,9705
12
+ tree_engine/core/journal_compose.py,sha256=wVmtRbaEj3LWiaZMezPrzfjhEJHkUr_Bt1K8nPDoJ_k,21766
13
+ tree_engine/core/live_tree.py,sha256=uN8FcvwOgJ-gxZQshs9wJv62vUnPa8RMtnJg0RiMNS8,32306
14
+ tree_engine/core/locking.py,sha256=bsVilRJB-rGhnb0lI3TGyjIkrLt93ZVNQBPp1l2nm9Y,8754
15
+ tree_engine/core/move.py,sha256=6gx9lWm9_xSyXmsSEFJVr_flvZRGJj0hGB-EHxIQQ9s,26378
16
+ tree_engine/core/move_references.py,sha256=mTLbrHIiV8tIm_mtAD52wDvozTsuegVyJMLRihNS-PI,13971
17
+ tree_engine/core/node_types.py,sha256=n8aZ-kQ-rbZbQhxV1_kTnevTU5SRnsPZZHbWTZp3344,17464
18
+ tree_engine/core/nodes.py,sha256=jvRt8T19Ba6yNqXWjaAUmVoZqUrRqPg7JosaQ09h38Y,14787
19
+ tree_engine/core/operations.py,sha256=_mOzRz5LIGWIR8a5BhT2C07Tus31lmRwakrAAUD6fE8,32461
20
+ tree_engine/core/plugin_boundary.py,sha256=KHseccyHmcHaPxpBykbmedzNU07nQ4MQg3z03fPAmb8,7196
21
+ tree_engine/core/position_map.py,sha256=JJM3cPM0KZG_oom2423nDCLKCtfJ2NKhqjHiSnIJX-o,9695
22
+ tree_engine/core/reference_cache.py,sha256=EdxaOiICUIc60jme1SVBhqC6zOi5TCbqOKcze_2cXVY,15023
23
+ tree_engine/core/references.py,sha256=61X6-3yX_qEtzXKAP0Galpw0co6gF77rOelfeGrTNKg,9907
24
+ tree_engine/core/reparse.py,sha256=kF_-qjJun53Ve1pBVZkX8nXxkxmSo7LB1JsiQkoRU_o,16898
25
+ tree_engine/core/short_id.py,sha256=3OrJRWQsnsqiQRvUEaNoiKchYKlZ0FwKWvPkhl9MGgU,11361
26
+ tree_engine/core/subtree_apply.py,sha256=D7cPUllR9bVEbXFLtiXOIe3-nDHQnXeVgnvC0B0r1v4,16244
27
+ tree_engine/core/subtree_copy.py,sha256=YsnW0jFYeyNlbJSJp_PyAcWZX8L54yTHuWoX3qubRAI,15967
28
+ tree_engine/core/transactions.py,sha256=krlPKMWUxp7wr2VARxQRz3Izo79xXy2zJ4_A-S15XxU,19006
29
+ tree_engine/core/trivia.py,sha256=qyd37VIJOdqn73lsD3Dm81t5QoavrgxOMz5Tp4NOqSE,16728
30
+ tree_engine/core/updates.py,sha256=41V9WvnAY9R9tE44DmZUpeC1V26T6OhCNTZnXN3fFts,18591
31
+ tree_engine/core/validation.py,sha256=ijHSIvhG3RVg3mm7jVsFU1Wkery-ucUD_cE7gvqi5ng,16249
32
+ tree_engine/plugins/contract.py,sha256=s_RTp4K6OdJX9rUfvQMZPQGC4U8YXf9FqqPnbBpHOQc,14292
33
+ tree_engine/plugins/detachable.py,sha256=3K9OMWqLY-7v55mOUuFrdCuJvXOXe9CuEpSvqxCuRFM,7064
34
+ tree_engine/plugins/fallback.py,sha256=9IS83CBANCQ5dtftS4-q97dzBcqEtAWDkDHNnXlUB8I,18044
35
+ tree_engine/plugins/ini_format.py,sha256=KlQhFm5YkrbMbb8YR_sRv-KSfdyMdMGqNtrpZu5Our8,11484
36
+ tree_engine/plugins/json_format.py,sha256=J-QzLdNQAZRLfLbeZ1ME-H6BxZq4eBcHB_yVUU-NBZc,40026
37
+ tree_engine/plugins/json_pointer.py,sha256=oXHcab0_BT2cDtJwmIrEvPa6CUoSFx-0vR3lTDggQBw,8185
38
+ tree_engine/plugins/plain_text.py,sha256=drGE9HCk12gq5hbiXGo1zgAUa0S8v0HerRDnNfyKN6o,55326
39
+ tree_engine/plugins/registry.py,sha256=o31HKHCAjuH7AwkL98i7LM9gi6ay0idqZwsF5Qj2VW4,17053
40
+ tree_engine/plugins/selection.py,sha256=aHNjt2jcDLc6cuCABfgNn3yFoTvY2tEmLqzeesVhNSY,9692
41
+ tree_engine/plugins/toml_format.py,sha256=JmSBv9yBTL8LGEaIRcB-gDnlLbaYZSgCEd0JlTdhrV0,26505
42
+ tree_engine/plugins/bsl/dialects.py,sha256=sv5nJdb2DVyZisqmh-CgVHQ9I6AHG1oy-VxxVjxgGa4,11856
43
+ tree_engine/plugins/bsl/generator.py,sha256=_PSAPmqyWDXK7jB9Lo6c9xmBJtc02ELa_KKFhfw3nmE,17821
44
+ tree_engine/plugins/bsl/import_map.py,sha256=us00evFEIrhOANusfxJ409TJ48CkMG9BQkkXnlnSXlc,16864
45
+ tree_engine/plugins/bsl/plugin.py,sha256=YJzR00TsMqcj2eOts2TfYaA7VnIMf9_8wImPI4Z28J0,16563
46
+ tree_engine/plugins/python/export_map.py,sha256=ZM0IWskuUogSgJLspmlLEH03577cWBaawXR0k7yQa2Q,14089
47
+ tree_engine/plugins/python/import_map.py,sha256=QEa4xJuoMjobN1wvcGot_pahI2dJTWxDf4Kpk56f8BM,18196
48
+ tree_engine/plugins/python/plugin.py,sha256=rYqgsGGFa8J1O8L7cR2FHxWqn4x-7B05GINM_2zW0Co,35589
49
+ tree_engine/plugins/yaml/builder.py,sha256=xbCU0fAGJeL38D9BWv-K8ihlThZPLxhWw9mPMGg6F9E,1972
50
+ tree_engine/plugins/yaml/emitter.py,sha256=bVBS-rZ7gEXjVEp6TQEs5S6iBxH3Tiirbf6-Wm2WsGo,8436
51
+ tree_engine/plugins/yaml/flow.py,sha256=BR4kdj02twSdX36mCbxRgWVPehivwOxZvdvEtn6M7wc,8627
52
+ tree_engine/plugins/yaml/plugin.py,sha256=vwwpKAluKbD_-I_ICOS--RzCXKAodlfPuPoRv1jEoVk,15037
53
+ tree_engine/plugins/yaml/reader.py,sha256=-GvuFgOx-SV5hl2WLe_kWaub2SZlY1ZPY_GmbFJ6TkI,16994
54
+ tree_engine/plugins/yaml/scanner.py,sha256=Cp77W_o-_0kgkE5XoBNvbgbtIb_u_Gy4wCzcWSuyrrc,12068
55
+ tree_engine/query/adapter.py,sha256=ba7Mp8pb6kqNgYE6ZoJNLcRpWnbS9fnd6TRw3u2uO4Y,12076
56
+ tree_engine/query/engine.py,sha256=9bxKSrAkygaXjaPyQly9WziDR-D8oYvy22UqL-FFINo,19827
57
+ tree_engine/query/inspection.py,sha256=6BiEe0XhDowfyjLsMBdKnPQCnLEbs0ahoE8R6h7td9c,23940
58
+ tree_engine/query/outline.py,sha256=aWcPP3Oz0CtJfk3ywBDVibD-POGNpcmkgbSiTILAfp4,12116
59
+ tree_engine/query/predicates.py,sha256=uWhsbUBI7EY5C_1-rLik0op4MKHgUGZ52VYgFP0Lj0U,9938
60
+ tree_engine/query/results.py,sha256=YhInAYJlrkEWU-JhFeCnfnp-SEEsRyTTEju3A4GoxbI,6267
61
+ tree_engine/query/selector.py,sha256=AqqrHVg1DfbwjXASTzRMCor1zj-2lQkjrN3gMM9afUg,13616
62
+ tree_engine/storage/codec.py,sha256=AAXEw2sdIVeqeexRuUsUFjXL0Ku5QmMsh2mMnN1GLOA,20421
63
+ tree_engine/storage/document_bridge.py,sha256=y5hRQI0ezrX2rDq8wz96wKkX-Yng2pKNFKmeODweIo4,29033
64
+ tree_engine/storage/file_txn.py,sha256=IUatv32ktdjnk0_JEH_-8JtXVlSTqSAbFUcCNbdS-3Y,13442
65
+ tree_engine/storage/history.py,sha256=8ymE2EC1GxWd-yUshFlDKmtt9x5QmzKSUSvp1H-81u4,5373
66
+ tree_engine/storage/lifecycle.py,sha256=mlJ2_nP24LSnAVG7v4yp1WO294iixuaFDxp7Q96Xmm8,32172
67
+ tree_engine/storage/schema.py,sha256=j-y1dcLTRk_YK3loO7fKUe9XwNzhJZwzBeKQ1aVaG2A,16908
68
+ tree_engine/storage/session_guard.py,sha256=Ohj5F5_-KVIGnjIbJpSN2wbGTQMJlZL_3MwPS2IMzVc,17947
69
+ ai_editor_tree_engine-1.0.147.dist-info/METADATA,sha256=Dmeby78BHWp3wQaGnRxk8ErY-LuYXC-etbZ9r9SqnCU,834
70
+ ai_editor_tree_engine-1.0.147.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
71
+ ai_editor_tree_engine-1.0.147.dist-info/top_level.txt,sha256=cWBMqIrMERXRH2Fh8V7A8SpvVXvZUQLDp2gDSpJlT1A,12
72
+ ai_editor_tree_engine-1.0.147.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Vasiliy Zdanovskiy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ tree_engine
@@ -0,0 +1,376 @@
1
+ """Canonical address-normalization layer for concept C-004 (NodeIdentityAndAddressing).
2
+
3
+ Scope (concept C-004, narrowed to string-form serialization of ``NodeAddress``
4
+ and the mandatory ``normalize_node_address`` entry point): this module owns
5
+ the ``to_str``/``from_str`` external string-form helpers for the imported
6
+ ``NodeAddress`` type, the single ``normalize_node_address`` function every
7
+ public node-address operation must call first per {j9rh}, the
8
+ ``NodeAddressError`` exception hierarchy, and the {p021} node-view helper
9
+ ``build_node_address_view``.
10
+
11
+ ACCEPTANCE NOTE (orchestrator integration decision): the canonical
12
+ ``NodeAddress`` and ``AddressRemap`` types are defined in the sibling module
13
+ ``tree_engine.core.identity`` -- this file imports ``NodeAddress`` from
14
+ there and never redefines it. This module is read-only with respect to
15
+ document state: no code path here mutates a short_id map, a node table, a
16
+ generation counter, or an allocator.
17
+
18
+ Sibling contract not yet available as an importable module in this worktree
19
+ -- the document-local bidirectional short_id<->UUID4 map (``short_id.py``,
20
+ delivered by a parallel T-001 artifact) -- is consumed here only as a
21
+ caller-injected, duck-typed callable, following the same pattern already
22
+ used by ``core/validation.py`` (``next_short_id``) and ``core/node_types.py``
23
+ (``index_map_hook``): this file never imports that sibling module and never
24
+ stubs a fake replacement for it.
25
+
26
+ ``document`` is consumed only through minimal, duck-typed attributes, the
27
+ same convention ``core/identity.py``'s ``replace_node_id`` and
28
+ ``core/validation.py``'s ``validate_mutation`` already rely on:
29
+
30
+ * ``document.nodes_by_id`` -- a mapping (supports ``in``) of canonical
31
+ ``uuid.UUID`` node_id to node object. Read-only here.
32
+ * ``document.resolve_short_id(short_id: int) -> Sequence[uuid.UUID]`` --
33
+ the short-id-map module's public accessor, consulted only when no
34
+ ``resolve_short_id`` callable is explicitly injected into
35
+ :func:`normalize_node_address`. It returns the sequence of node_id
36
+ matches for ``short_id`` (normally zero or one; more than one is treated
37
+ defensively as an ambiguous resolution rather than assumed impossible).
38
+
39
+ Source-of-truth requirement labels honored here: {j9rh}, {p021}, {p080}.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from dataclasses import dataclass
45
+ from typing import Any, Callable, Optional, Sequence, Union
46
+ from uuid import UUID
47
+
48
+ from tree_engine.core.identity import NodeAddress
49
+
50
+ __all__ = [
51
+ "to_str",
52
+ "serialize",
53
+ "from_str",
54
+ "parse",
55
+ "NodeAddressError",
56
+ "UnknownAddressError",
57
+ "AmbiguousAddressError",
58
+ "ForeignDocumentAddressError",
59
+ "ShortIdResolver",
60
+ "normalize_node_address",
61
+ "NodeAddressView",
62
+ "build_node_address_view",
63
+ ]
64
+
65
+ # Duck-typed accessor injected by a caller (or looked up on ``document`` as
66
+ # ``document.resolve_short_id``) to resolve a positive-int short_id to the
67
+ # sequence of matching canonical node_id values. See module docstring.
68
+ ShortIdResolver = Callable[[int], Sequence[UUID]]
69
+
70
+ _HEX_PREFIXES = ("0x", "0X")
71
+
72
+
73
+ # --------------------------------------------------------------------------
74
+ # External string-form serialization ({p080} explicit round-trip form)
75
+ # --------------------------------------------------------------------------
76
+
77
+
78
+ def to_str(address: NodeAddress) -> str:
79
+ """Serialize ``address`` to its ``"document_id:node_id"`` string form.
80
+
81
+ Both parts are always emitted explicitly as canonical hyphenated UUID4
82
+ strings, regardless of the implicit-local in-memory form ``NodeAddress``
83
+ otherwise permits. Raises ``ValueError`` when ``address.document_id`` is
84
+ ``None`` (the implicit-local form), since no document identifier is
85
+ then available to emit.
86
+ """
87
+
88
+ if address.document_id is None:
89
+ raise ValueError(
90
+ "cannot serialize NodeAddress to its string form: document_id "
91
+ "is None (implicit-local form carries no explicit document_id)"
92
+ )
93
+ return f"{address.document_id}:{address.node_id}"
94
+
95
+
96
+ # Alias, per the step spec's "to_str/serialize" naming.
97
+ serialize = to_str
98
+
99
+
100
+ def from_str(text: str) -> NodeAddress:
101
+ """Parse the ``"document_id:node_id"`` string form into a ``NodeAddress``.
102
+
103
+ Both parts are required. Raises ``ValueError`` for any malformed input:
104
+ a non-string argument, a missing or extra ``":"`` separator, an empty
105
+ segment, or a segment that is not a syntactically valid UUID.
106
+ """
107
+
108
+ if not isinstance(text, str):
109
+ raise ValueError(f"expected a str, got {type(text).__name__}")
110
+
111
+ parts = text.split(":")
112
+ if len(parts) != 2:
113
+ raise ValueError(
114
+ f"malformed node address {text!r}: expected exactly one ':' "
115
+ f"separator (document_id:node_id), found {len(parts) - 1}"
116
+ )
117
+
118
+ document_part, node_part = parts
119
+ if not document_part or not node_part:
120
+ raise ValueError(f"malformed node address {text!r}: empty segment")
121
+
122
+ try:
123
+ document_id = UUID(document_part)
124
+ node_id = UUID(node_part)
125
+ except (ValueError, AttributeError, TypeError) as exc:
126
+ raise ValueError(f"malformed node address {text!r}: invalid UUID segment") from exc
127
+
128
+ return NodeAddress(document_id=document_id, node_id=node_id)
129
+
130
+
131
+ # Alias, per the step spec's "from_str/parse" naming.
132
+ parse = from_str
133
+
134
+
135
+ # --------------------------------------------------------------------------
136
+ # Exception hierarchy
137
+ # --------------------------------------------------------------------------
138
+
139
+
140
+ class NodeAddressError(Exception):
141
+ """Base error for a rejected node-address normalization.
142
+
143
+ Always carries the offending raw ``address`` argument exactly as given
144
+ to :func:`normalize_node_address`, so a caller can inspect what was
145
+ rejected without re-parsing an error message.
146
+ """
147
+
148
+ def __init__(self, raw_address: Any, message: Optional[str] = None) -> None:
149
+ self.raw_address = raw_address
150
+ super().__init__(message or f"invalid node address: {raw_address!r}")
151
+
152
+
153
+ class UnknownAddressError(NodeAddressError):
154
+ """Raised when ``address`` cannot be resolved to any node of the document.
155
+
156
+ Covers a UUID4 node_id absent from the document's node table and an
157
+ int/hex short_id absent from the document's short_id map.
158
+ """
159
+
160
+
161
+ class AmbiguousAddressError(NodeAddressError):
162
+ """Raised when short_id resolution defensively detects more than one match.
163
+
164
+ The short_id<->UUID4 map is a bidirectional map and should never
165
+ legitimately produce more than one match for a single short_id; this
166
+ error exists purely as a defensive guard against that invariant being
167
+ violated, rather than an assumption that it cannot happen.
168
+ """
169
+
170
+
171
+ class ForeignDocumentAddressError(NodeAddressError):
172
+ """Raised when ``address`` explicitly names a document_id other than the current one.
173
+
174
+ Raised immediately, before any local short_id-map or node-table lookup
175
+ is attempted -- ``document_id`` carries the offending foreign document
176
+ identifier.
177
+ """
178
+
179
+ def __init__(self, raw_address: Any, document_id: UUID, message: Optional[str] = None) -> None:
180
+ self.document_id = document_id
181
+ super().__init__(
182
+ raw_address,
183
+ message
184
+ or (
185
+ f"node address {raw_address!r} names foreign document_id "
186
+ f"{document_id!r}"
187
+ ),
188
+ )
189
+
190
+
191
+ # --------------------------------------------------------------------------
192
+ # normalize_node_address -- the single mandatory entry point per {j9rh}
193
+ # --------------------------------------------------------------------------
194
+
195
+
196
+ def _validate_node_id_presence(document: Any, node_id: UUID, raw_address: Any) -> UUID:
197
+ """Confirm ``node_id`` is present in ``document.nodes_by_id``, or reject.
198
+
199
+ Pure lookup: never mutates ``document`` or anything reachable from it.
200
+ """
201
+
202
+ nodes_by_id = getattr(document, "nodes_by_id", None)
203
+ if nodes_by_id is None or node_id not in nodes_by_id:
204
+ raise UnknownAddressError(raw_address)
205
+ return node_id
206
+
207
+
208
+ def _resolve_node_address_value(
209
+ document: Any,
210
+ node_address: NodeAddress,
211
+ raw_address: Any,
212
+ current_document_id: Optional[UUID],
213
+ ) -> UUID:
214
+ """Apply the foreign-document check, then validate the local node_id."""
215
+
216
+ if node_address.document_id is not None and node_address.document_id != current_document_id:
217
+ raise ForeignDocumentAddressError(raw_address, node_address.document_id)
218
+ return _validate_node_id_presence(document, node_address.node_id, raw_address)
219
+
220
+
221
+ def _resolve_short_id(
222
+ document: Any,
223
+ short_id: int,
224
+ raw_address: Any,
225
+ resolve_short_id: Optional[ShortIdResolver],
226
+ ) -> UUID:
227
+ """Resolve a positive-int short_id via the injected or duck-typed accessor.
228
+
229
+ Never touches the short-id-map's internal storage directly: it only
230
+ calls the public accessor, either explicitly injected by the caller or
231
+ found as ``document.resolve_short_id``. Read-only: performs no
232
+ mutation regardless of outcome.
233
+ """
234
+
235
+ resolver = resolve_short_id if resolve_short_id is not None else getattr(
236
+ document, "resolve_short_id", None
237
+ )
238
+ if resolver is None:
239
+ # No accessor available at all: the address cannot be resolved.
240
+ raise UnknownAddressError(raw_address)
241
+
242
+ matches = list(resolver(short_id))
243
+ if len(matches) == 0:
244
+ raise UnknownAddressError(raw_address)
245
+ if len(matches) > 1:
246
+ raise AmbiguousAddressError(raw_address)
247
+ return matches[0]
248
+
249
+
250
+ def normalize_node_address(
251
+ document: Any,
252
+ address: Union[UUID, int, str, NodeAddress],
253
+ *,
254
+ current_document_id: Optional[UUID],
255
+ resolve_short_id: Optional[ShortIdResolver] = None,
256
+ ) -> UUID:
257
+ """Resolve any accepted address form to its canonical ``uuid.UUID`` node_id.
258
+
259
+ The single mandatory entry point every public node-address operation
260
+ must call first, per {j9rh}. Accepts ``address`` as:
261
+
262
+ * ``uuid.UUID`` -- a canonical node_id, validated against
263
+ ``document.nodes_by_id``.
264
+ * ``int`` -- a positive document-local short_id, resolved through the
265
+ short_id map.
266
+ * ``str`` -- a ``"0x"``-prefixed hex short_id, a DECIMAL-digit short_id,
267
+ a bare UUID string, or a serialized ``"document_id:node_id"``
268
+ ``NodeAddress`` (see :func:`from_str`). The decimal form is tried
269
+ before the UUID form and resolves exactly like the ``int`` it spells,
270
+ so ``"3"`` and ``3`` are the same address: callers that carry a
271
+ short_id through a string-typed protocol field -- the editor's
272
+ ``node_ref`` is one -- would otherwise fall through every branch and
273
+ be rejected as unknown. Only ASCII digits count, so a non-ASCII
274
+ decimal digit is not silently reinterpreted.
275
+ * ``NodeAddress`` -- resolved directly.
276
+
277
+ A ``NodeAddress``-shaped address (an explicit ``NodeAddress`` instance
278
+ or a serialized string form) whose ``document_id`` differs from
279
+ ``current_document_id`` raises :class:`ForeignDocumentAddressError`
280
+ immediately, before any local short_id-map or node-table lookup. An
281
+ unresolvable UUID or short_id raises :class:`UnknownAddressError`. A
282
+ defensively-detected multi-match short_id resolution raises
283
+ :class:`AmbiguousAddressError`. Every rejection path is read-only: no
284
+ mutation of the short_id map, node table, generation counters, or
285
+ allocator state, and no partial operation is ever started.
286
+
287
+ Returns the canonical ``uuid.UUID`` node_id -- the canonicalization
288
+ target per {j9rh} -- never a ``NodeAddress``.
289
+ """
290
+
291
+ raw_address = address
292
+
293
+ if isinstance(address, NodeAddress):
294
+ return _resolve_node_address_value(document, address, raw_address, current_document_id)
295
+
296
+ if isinstance(address, UUID):
297
+ return _validate_node_id_presence(document, address, raw_address)
298
+
299
+ if isinstance(address, bool):
300
+ # bool is a subclass of int in Python; explicitly not an accepted
301
+ # address form.
302
+ raise UnknownAddressError(raw_address)
303
+
304
+ if isinstance(address, int):
305
+ if address <= 0:
306
+ raise UnknownAddressError(raw_address)
307
+ return _resolve_short_id(document, address, raw_address, resolve_short_id)
308
+
309
+ if isinstance(address, str):
310
+ if address.startswith(_HEX_PREFIXES):
311
+ try:
312
+ short_id = int(address, 16)
313
+ except ValueError:
314
+ raise UnknownAddressError(raw_address) from None
315
+ if short_id <= 0:
316
+ raise UnknownAddressError(raw_address)
317
+ return _resolve_short_id(document, short_id, raw_address, resolve_short_id)
318
+
319
+ if ":" in address:
320
+ try:
321
+ parsed_address = from_str(address)
322
+ except ValueError:
323
+ raise UnknownAddressError(raw_address) from None
324
+ return _resolve_node_address_value(
325
+ document, parsed_address, raw_address, current_document_id
326
+ )
327
+
328
+ if address.isascii() and address.isdigit():
329
+ short_id = int(address)
330
+ if short_id <= 0:
331
+ raise UnknownAddressError(raw_address)
332
+ return _resolve_short_id(document, short_id, raw_address, resolve_short_id)
333
+
334
+ try:
335
+ node_id = UUID(address)
336
+ except ValueError:
337
+ raise UnknownAddressError(raw_address) from None
338
+ return _validate_node_id_presence(document, node_id, raw_address)
339
+
340
+ raise UnknownAddressError(raw_address)
341
+
342
+
343
+ # --------------------------------------------------------------------------
344
+ # View contract per {p021}
345
+ # --------------------------------------------------------------------------
346
+
347
+
348
+ @dataclass(frozen=True)
349
+ class NodeAddressView:
350
+ """The pair of identifiers {p021} requires every node view to expose.
351
+
352
+ ``short_id_hex`` is the compact, ``"0x"``-prefixed hexadecimal
353
+ rendering used for later re-addressing; ``node_id`` is the canonical
354
+ UUID4 string. ``short_id_hex`` never replaces ``node_id`` -- both are
355
+ always present together.
356
+ """
357
+
358
+ short_id_hex: str
359
+ node_id: str
360
+
361
+
362
+ def build_node_address_view(node_id: Union[UUID, str], short_id: int) -> NodeAddressView:
363
+ """Build the {p021} view pair from a canonical node_id and its short_id.
364
+
365
+ ``node_id`` may be given as a ``uuid.UUID`` or its canonical string
366
+ form; ``short_id`` is the positive document-local integer id. Every
367
+ node listing/view assembler elsewhere in the tree engine is expected to
368
+ call this helper so ``short_id_hex`` and ``node_id`` are always
369
+ populated together.
370
+ """
371
+
372
+ node_id_value = node_id if isinstance(node_id, UUID) else UUID(str(node_id))
373
+ return NodeAddressView(
374
+ short_id_hex=f"0x{int(short_id):x}",
375
+ node_id=str(node_id_value),
376
+ )