ai-editor-tree-engine 1.0.147__tar.gz

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 (78) hide show
  1. ai_editor_tree_engine-1.0.147/LICENSE +21 -0
  2. ai_editor_tree_engine-1.0.147/MANIFEST.in +3 -0
  3. ai_editor_tree_engine-1.0.147/PKG-INFO +19 -0
  4. ai_editor_tree_engine-1.0.147/VERSION +1 -0
  5. ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/PKG-INFO +19 -0
  6. ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/SOURCES.txt +76 -0
  7. ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/dependency_links.txt +1 -0
  8. ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/requires.txt +13 -0
  9. ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/top_level.txt +1 -0
  10. ai_editor_tree_engine-1.0.147/pyproject.toml +100 -0
  11. ai_editor_tree_engine-1.0.147/setup.cfg +4 -0
  12. ai_editor_tree_engine-1.0.147/tree_engine/core/address.py +376 -0
  13. ai_editor_tree_engine-1.0.147/tree_engine/core/identifier_map.py +266 -0
  14. ai_editor_tree_engine-1.0.147/tree_engine/core/identity.py +226 -0
  15. ai_editor_tree_engine-1.0.147/tree_engine/core/instrumentation.py +399 -0
  16. ai_editor_tree_engine-1.0.147/tree_engine/core/integrity.py +341 -0
  17. ai_editor_tree_engine-1.0.147/tree_engine/core/integrity_full.py +382 -0
  18. ai_editor_tree_engine-1.0.147/tree_engine/core/journal.py +260 -0
  19. ai_editor_tree_engine-1.0.147/tree_engine/core/journal_compose.py +568 -0
  20. ai_editor_tree_engine-1.0.147/tree_engine/core/live_tree.py +672 -0
  21. ai_editor_tree_engine-1.0.147/tree_engine/core/locking.py +188 -0
  22. ai_editor_tree_engine-1.0.147/tree_engine/core/move.py +518 -0
  23. ai_editor_tree_engine-1.0.147/tree_engine/core/move_references.py +323 -0
  24. ai_editor_tree_engine-1.0.147/tree_engine/core/node_types.py +408 -0
  25. ai_editor_tree_engine-1.0.147/tree_engine/core/nodes.py +348 -0
  26. ai_editor_tree_engine-1.0.147/tree_engine/core/operations.py +609 -0
  27. ai_editor_tree_engine-1.0.147/tree_engine/core/plugin_boundary.py +160 -0
  28. ai_editor_tree_engine-1.0.147/tree_engine/core/position_map.py +263 -0
  29. ai_editor_tree_engine-1.0.147/tree_engine/core/reference_cache.py +360 -0
  30. ai_editor_tree_engine-1.0.147/tree_engine/core/references.py +223 -0
  31. ai_editor_tree_engine-1.0.147/tree_engine/core/reparse.py +378 -0
  32. ai_editor_tree_engine-1.0.147/tree_engine/core/short_id.py +296 -0
  33. ai_editor_tree_engine-1.0.147/tree_engine/core/subtree_apply.py +397 -0
  34. ai_editor_tree_engine-1.0.147/tree_engine/core/subtree_copy.py +399 -0
  35. ai_editor_tree_engine-1.0.147/tree_engine/core/transactions.py +397 -0
  36. ai_editor_tree_engine-1.0.147/tree_engine/core/trivia.py +396 -0
  37. ai_editor_tree_engine-1.0.147/tree_engine/core/updates.py +398 -0
  38. ai_editor_tree_engine-1.0.147/tree_engine/core/validation.py +377 -0
  39. ai_editor_tree_engine-1.0.147/tree_engine/errors.py +95 -0
  40. ai_editor_tree_engine-1.0.147/tree_engine/exceptions.py +326 -0
  41. ai_editor_tree_engine-1.0.147/tree_engine/facade.py +2723 -0
  42. ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/dialects.py +303 -0
  43. ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/generator.py +401 -0
  44. ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/import_map.py +371 -0
  45. ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/plugin.py +366 -0
  46. ai_editor_tree_engine-1.0.147/tree_engine/plugins/contract.py +351 -0
  47. ai_editor_tree_engine-1.0.147/tree_engine/plugins/detachable.py +123 -0
  48. ai_editor_tree_engine-1.0.147/tree_engine/plugins/fallback.py +421 -0
  49. ai_editor_tree_engine-1.0.147/tree_engine/plugins/ini_format.py +317 -0
  50. ai_editor_tree_engine-1.0.147/tree_engine/plugins/json_format.py +789 -0
  51. ai_editor_tree_engine-1.0.147/tree_engine/plugins/json_pointer.py +203 -0
  52. ai_editor_tree_engine-1.0.147/tree_engine/plugins/plain_text.py +1182 -0
  53. ai_editor_tree_engine-1.0.147/tree_engine/plugins/python/export_map.py +306 -0
  54. ai_editor_tree_engine-1.0.147/tree_engine/plugins/python/import_map.py +357 -0
  55. ai_editor_tree_engine-1.0.147/tree_engine/plugins/python/plugin.py +695 -0
  56. ai_editor_tree_engine-1.0.147/tree_engine/plugins/registry.py +399 -0
  57. ai_editor_tree_engine-1.0.147/tree_engine/plugins/selection.py +228 -0
  58. ai_editor_tree_engine-1.0.147/tree_engine/plugins/toml_format.py +549 -0
  59. ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/builder.py +49 -0
  60. ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/emitter.py +190 -0
  61. ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/flow.py +196 -0
  62. ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/plugin.py +317 -0
  63. ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/reader.py +359 -0
  64. ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/scanner.py +289 -0
  65. ai_editor_tree_engine-1.0.147/tree_engine/query/adapter.py +265 -0
  66. ai_editor_tree_engine-1.0.147/tree_engine/query/engine.py +424 -0
  67. ai_editor_tree_engine-1.0.147/tree_engine/query/inspection.py +498 -0
  68. ai_editor_tree_engine-1.0.147/tree_engine/query/outline.py +288 -0
  69. ai_editor_tree_engine-1.0.147/tree_engine/query/predicates.py +236 -0
  70. ai_editor_tree_engine-1.0.147/tree_engine/query/results.py +160 -0
  71. ai_editor_tree_engine-1.0.147/tree_engine/query/selector.py +400 -0
  72. ai_editor_tree_engine-1.0.147/tree_engine/storage/codec.py +400 -0
  73. ai_editor_tree_engine-1.0.147/tree_engine/storage/document_bridge.py +473 -0
  74. ai_editor_tree_engine-1.0.147/tree_engine/storage/file_txn.py +351 -0
  75. ai_editor_tree_engine-1.0.147/tree_engine/storage/history.py +106 -0
  76. ai_editor_tree_engine-1.0.147/tree_engine/storage/lifecycle.py +572 -0
  77. ai_editor_tree_engine-1.0.147/tree_engine/storage/schema.py +403 -0
  78. ai_editor_tree_engine-1.0.147/tree_engine/storage/session_guard.py +398 -0
@@ -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,3 @@
1
+ include VERSION
2
+ include LICENSE
3
+ recursive-include tree_engine *.py
@@ -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 @@
1
+ 1.0.147
@@ -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,76 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ VERSION
4
+ pyproject.toml
5
+ ai_editor_tree_engine.egg-info/PKG-INFO
6
+ ai_editor_tree_engine.egg-info/SOURCES.txt
7
+ ai_editor_tree_engine.egg-info/dependency_links.txt
8
+ ai_editor_tree_engine.egg-info/requires.txt
9
+ ai_editor_tree_engine.egg-info/top_level.txt
10
+ tree_engine/errors.py
11
+ tree_engine/exceptions.py
12
+ tree_engine/facade.py
13
+ tree_engine/core/address.py
14
+ tree_engine/core/identifier_map.py
15
+ tree_engine/core/identity.py
16
+ tree_engine/core/instrumentation.py
17
+ tree_engine/core/integrity.py
18
+ tree_engine/core/integrity_full.py
19
+ tree_engine/core/journal.py
20
+ tree_engine/core/journal_compose.py
21
+ tree_engine/core/live_tree.py
22
+ tree_engine/core/locking.py
23
+ tree_engine/core/move.py
24
+ tree_engine/core/move_references.py
25
+ tree_engine/core/node_types.py
26
+ tree_engine/core/nodes.py
27
+ tree_engine/core/operations.py
28
+ tree_engine/core/plugin_boundary.py
29
+ tree_engine/core/position_map.py
30
+ tree_engine/core/reference_cache.py
31
+ tree_engine/core/references.py
32
+ tree_engine/core/reparse.py
33
+ tree_engine/core/short_id.py
34
+ tree_engine/core/subtree_apply.py
35
+ tree_engine/core/subtree_copy.py
36
+ tree_engine/core/transactions.py
37
+ tree_engine/core/trivia.py
38
+ tree_engine/core/updates.py
39
+ tree_engine/core/validation.py
40
+ tree_engine/plugins/contract.py
41
+ tree_engine/plugins/detachable.py
42
+ tree_engine/plugins/fallback.py
43
+ tree_engine/plugins/ini_format.py
44
+ tree_engine/plugins/json_format.py
45
+ tree_engine/plugins/json_pointer.py
46
+ tree_engine/plugins/plain_text.py
47
+ tree_engine/plugins/registry.py
48
+ tree_engine/plugins/selection.py
49
+ tree_engine/plugins/toml_format.py
50
+ tree_engine/plugins/bsl/dialects.py
51
+ tree_engine/plugins/bsl/generator.py
52
+ tree_engine/plugins/bsl/import_map.py
53
+ tree_engine/plugins/bsl/plugin.py
54
+ tree_engine/plugins/python/export_map.py
55
+ tree_engine/plugins/python/import_map.py
56
+ tree_engine/plugins/python/plugin.py
57
+ tree_engine/plugins/yaml/builder.py
58
+ tree_engine/plugins/yaml/emitter.py
59
+ tree_engine/plugins/yaml/flow.py
60
+ tree_engine/plugins/yaml/plugin.py
61
+ tree_engine/plugins/yaml/reader.py
62
+ tree_engine/plugins/yaml/scanner.py
63
+ tree_engine/query/adapter.py
64
+ tree_engine/query/engine.py
65
+ tree_engine/query/inspection.py
66
+ tree_engine/query/outline.py
67
+ tree_engine/query/predicates.py
68
+ tree_engine/query/results.py
69
+ tree_engine/query/selector.py
70
+ tree_engine/storage/codec.py
71
+ tree_engine/storage/document_bridge.py
72
+ tree_engine/storage/file_txn.py
73
+ tree_engine/storage/history.py
74
+ tree_engine/storage/lifecycle.py
75
+ tree_engine/storage/schema.py
76
+ tree_engine/storage/session_guard.py
@@ -0,0 +1,13 @@
1
+ lark<2.0,>=1.3.1
2
+
3
+ [tree-engine]
4
+ libcst<2.0,>=1.8.6
5
+ tree-sitter<0.26,>=0.25
6
+ tree-sitter-bsl<0.2,>=0.1.6
7
+
8
+ [tree-engine-bsl]
9
+ tree-sitter<0.26,>=0.25
10
+ tree-sitter-bsl<0.2,>=0.1.6
11
+
12
+ [tree-engine-python]
13
+ libcst<2.0,>=1.8.6
@@ -0,0 +1,100 @@
1
+ [build-system]
2
+ # setuptools>=77 is required for PEP 639 (`license = "MIT"` as an SPDX string
3
+ # plus `license-files`); the root distribution still builds on setuptools>=69.
4
+ requires = ["setuptools>=77", "wheel"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "ai-editor-tree-engine"
9
+ dynamic = ["version"]
10
+ description = "Format-independent parse/tree/identity/render engine used as the ai-editor server's internals"
11
+ requires-python = ">=3.10"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ authors = [
15
+ { name = "Vasiliy Zdanovskiy", email = "vasilyvz@gmail.com" },
16
+ ]
17
+ # One base dependency, and it is not a format backend. `lark` backs
18
+ # `tree_engine.facade.query`: `facade.py` imports `tree_engine.query.engine`
19
+ # unconditionally at module scope, which reaches
20
+ # `tree_engine/query/selector.py -> from lark import ...`, so WITHOUT lark
21
+ # `import tree_engine.facade` fails outright and the whole engine is
22
+ # unimportable. It was declared as the optional extra `tree-engine-query` until
23
+ # this release; that extra was a fiction, measured and removed rather than
24
+ # papered over. The alternative -- detaching the query engine the way the
25
+ # python and bsl FORMAT plugins are detached (see below) -- was rejected by the
26
+ # owner: `query` is part of the engine's declared API surface, not a format, so
27
+ # detaching it would shrink the API to satisfy a packaging label. lark is small,
28
+ # pure-Python and stable, unlike the two backends that churn.
29
+ #
30
+ # Everything else the core, the storage layer and the plain_text /
31
+ # json_format / toml_format / yaml plugins need is stdlib. Every FORMAT backend
32
+ # lives in the extras below.
33
+ dependencies = ["lark>=1.3.1,<2.0"]
34
+
35
+ [project.optional-dependencies]
36
+ # --- tree_engine backend extras -------------------------------------------
37
+ # src/tree_engine/ is format-independent by construction: pipeline/checks/
38
+ # check_boundary.py statically rejects any import of libcst / ast / tree_sitter /
39
+ # tree_sitter_bsl outside src/tree_engine/plugins/. The backends below are
40
+ # therefore genuinely optional -- the core, the storage layer and the
41
+ # plain_text / json_format / toml_format plugins are stdlib-only and need none
42
+ # of them. Install only the translator(s) you use.
43
+ #
44
+ # Version lines below are the MEASURED, round-trip-verified ones in this
45
+ # checkout, not copied from the plan text; see docs/release_identity.md §7 and
46
+ # docs/release_manifest.md ("Installed third-party versions").
47
+ #
48
+ # BACKEND MAJOR-UPGRADE RE-GATE RULE ({p032}): a MAJOR-version change of any
49
+ # third-party dependency pinned by this distribution -- the backends below
50
+ # (libcst, tree-sitter, tree-sitter-bsl) AND the base `lark` above -- REQUIRES
51
+ # re-running the contract and round-trip gate before publication. The upper
52
+ # bounds are what makes that rule mechanical: crossing one is an explicit,
53
+ # reviewed edit of this file, never an accidental resolver upgrade.
54
+ #
55
+ # libcst: 1.8.6 is installed and verified here (byte-identical round-trip of a
56
+ # real module through tree_engine.plugins.python.plugin.PythonFormatPlugin).
57
+ tree-engine-python = ["libcst>=1.8.6,<2.0"]
58
+ # tree-sitter: DELIBERATE DIVERGENCE from the plan's "0.24.x" pin. The BSL
59
+ # grammar shipped by tree-sitter-bsl 0.1.6 reports ABI version 15
60
+ # (tree_sitter.Language(tree_sitter_bsl.language()).abi_version == 15). The
61
+ # installed tree-sitter 0.25.2 declares LANGUAGE_VERSION == 15 with
62
+ # MIN_COMPATIBLE_LANGUAGE_VERSION == 13; the 0.24 line tops out below 15 and
63
+ # cannot load this grammar. Verified by execution: BSLFormatPlugin parses a real
64
+ # 1C module on 0.25.2 and renders it back byte-identically.
65
+ tree-engine-bsl = [
66
+ "tree-sitter>=0.25,<0.26",
67
+ "tree-sitter-bsl>=0.1.6,<0.2",
68
+ ]
69
+ # There is deliberately NO `tree-engine-query` extra any more: lark is a BASE
70
+ # dependency above, because the facade cannot be imported without it. An extra
71
+ # naming a dependency the distribution installs unconditionally would say the
72
+ # opposite of what is true.
73
+ # Aggregate: every tree_engine FORMAT backend at once.
74
+ tree-engine = [
75
+ "libcst>=1.8.6,<2.0",
76
+ "tree-sitter>=0.25,<0.26",
77
+ "tree-sitter-bsl>=0.1.6,<0.2",
78
+ ]
79
+
80
+ [tool.setuptools.dynamic]
81
+ # OWNER DECISION 2026-09-03: "the engine lives its own life" -- the engine
82
+ # versions INDEPENDENTLY of the editor. `VERSION` here is this distribution's
83
+ # OWN real file (not a symlink into the repository root any more): the
84
+ # engine's release number, bumped on the engine's own schedule. Only the
85
+ # server (`ai-editor`) and the client (`ai-editor-client`) stay version-locked
86
+ # to the repository-root VERSION file; this engine distribution does not. The
87
+ # root `pyproject.toml` pins `ai-editor-tree-engine==<this file's content>`
88
+ # exactly, so the two numbers can (and, over time, will) differ -- the server
89
+ # tracks whichever engine release it has verified against, not "the same
90
+ # number". See tests/unit/test_version_pinning.py and
91
+ # docs/release_identity.md ("Ratified 2026-09-03") for the enforced law.
92
+ version = { file = "VERSION" }
93
+
94
+ # `namespaces = true` is load-bearing, not decoration: tree_engine/ has no
95
+ # __init__.py anywhere, so every subpackage is an implicit namespace package
96
+ # (PEP 420) and a regular-package finder would ship an empty wheel.
97
+ [tool.setuptools.packages.find]
98
+ where = ["."]
99
+ include = ["tree_engine*"]
100
+ namespaces = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )