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.
- ai_editor_tree_engine-1.0.147/LICENSE +21 -0
- ai_editor_tree_engine-1.0.147/MANIFEST.in +3 -0
- ai_editor_tree_engine-1.0.147/PKG-INFO +19 -0
- ai_editor_tree_engine-1.0.147/VERSION +1 -0
- ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/PKG-INFO +19 -0
- ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/SOURCES.txt +76 -0
- ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/dependency_links.txt +1 -0
- ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/requires.txt +13 -0
- ai_editor_tree_engine-1.0.147/ai_editor_tree_engine.egg-info/top_level.txt +1 -0
- ai_editor_tree_engine-1.0.147/pyproject.toml +100 -0
- ai_editor_tree_engine-1.0.147/setup.cfg +4 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/address.py +376 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/identifier_map.py +266 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/identity.py +226 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/instrumentation.py +399 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/integrity.py +341 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/integrity_full.py +382 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/journal.py +260 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/journal_compose.py +568 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/live_tree.py +672 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/locking.py +188 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/move.py +518 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/move_references.py +323 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/node_types.py +408 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/nodes.py +348 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/operations.py +609 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/plugin_boundary.py +160 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/position_map.py +263 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/reference_cache.py +360 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/references.py +223 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/reparse.py +378 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/short_id.py +296 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/subtree_apply.py +397 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/subtree_copy.py +399 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/transactions.py +397 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/trivia.py +396 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/updates.py +398 -0
- ai_editor_tree_engine-1.0.147/tree_engine/core/validation.py +377 -0
- ai_editor_tree_engine-1.0.147/tree_engine/errors.py +95 -0
- ai_editor_tree_engine-1.0.147/tree_engine/exceptions.py +326 -0
- ai_editor_tree_engine-1.0.147/tree_engine/facade.py +2723 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/dialects.py +303 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/generator.py +401 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/import_map.py +371 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/bsl/plugin.py +366 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/contract.py +351 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/detachable.py +123 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/fallback.py +421 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/ini_format.py +317 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/json_format.py +789 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/json_pointer.py +203 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/plain_text.py +1182 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/python/export_map.py +306 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/python/import_map.py +357 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/python/plugin.py +695 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/registry.py +399 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/selection.py +228 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/toml_format.py +549 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/builder.py +49 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/emitter.py +190 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/flow.py +196 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/plugin.py +317 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/reader.py +359 -0
- ai_editor_tree_engine-1.0.147/tree_engine/plugins/yaml/scanner.py +289 -0
- ai_editor_tree_engine-1.0.147/tree_engine/query/adapter.py +265 -0
- ai_editor_tree_engine-1.0.147/tree_engine/query/engine.py +424 -0
- ai_editor_tree_engine-1.0.147/tree_engine/query/inspection.py +498 -0
- ai_editor_tree_engine-1.0.147/tree_engine/query/outline.py +288 -0
- ai_editor_tree_engine-1.0.147/tree_engine/query/predicates.py +236 -0
- ai_editor_tree_engine-1.0.147/tree_engine/query/results.py +160 -0
- ai_editor_tree_engine-1.0.147/tree_engine/query/selector.py +400 -0
- ai_editor_tree_engine-1.0.147/tree_engine/storage/codec.py +400 -0
- ai_editor_tree_engine-1.0.147/tree_engine/storage/document_bridge.py +473 -0
- ai_editor_tree_engine-1.0.147/tree_engine/storage/file_txn.py +351 -0
- ai_editor_tree_engine-1.0.147/tree_engine/storage/history.py +106 -0
- ai_editor_tree_engine-1.0.147/tree_engine/storage/lifecycle.py +572 -0
- ai_editor_tree_engine-1.0.147/tree_engine/storage/schema.py +403 -0
- 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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tree_engine
|
|
@@ -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,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
|
+
)
|