zeromodel-navigation 1.1.0__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.
- zeromodel/navigation/__init__.py +40 -0
- zeromodel/navigation/compiler.py +362 -0
- zeromodel/navigation/dto.py +584 -0
- zeromodel/navigation/errors.py +11 -0
- zeromodel/navigation/rules.py +149 -0
- zeromodel/navigation/storage.py +162 -0
- zeromodel/navigation/traversal.py +174 -0
- zeromodel_navigation-1.1.0.dist-info/METADATA +123 -0
- zeromodel_navigation-1.1.0.dist-info/RECORD +11 -0
- zeromodel_navigation-1.1.0.dist-info/WHEEL +5 -0
- zeromodel_navigation-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""zeromodel-navigation: finite, deterministic artifact-corpus hierarchy.
|
|
2
|
+
|
|
3
|
+
Depends only on `zeromodel` (core) and `zeromodel-artifacts`. Does not
|
|
4
|
+
depend on, or import, `zeromodel.trust`.
|
|
5
|
+
|
|
6
|
+
The public surface is deliberately restricted to the 12 names in
|
|
7
|
+
`__all__`. Supporting DTOs (`TileCoverageDTO`, `TilePointerDTO`,
|
|
8
|
+
`LeafBindingDTO`, `TraversalRuleDescriptorDTO`, `TraversalFailureDTO`) and
|
|
9
|
+
the reference rule implementations remain available from
|
|
10
|
+
`zeromodel.navigation.dto` / `zeromodel.navigation.rules` for tests and
|
|
11
|
+
advanced composition.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from zeromodel.navigation.compiler import compile_hierarchy, validate_hierarchy
|
|
15
|
+
from zeromodel.navigation.dto import (
|
|
16
|
+
HierarchyCompilerSpecDTO,
|
|
17
|
+
HierarchyManifestDTO,
|
|
18
|
+
NavigationTileDTO,
|
|
19
|
+
TraversalReceiptDTO,
|
|
20
|
+
TraversalRequestDTO,
|
|
21
|
+
TraversalResultDTO,
|
|
22
|
+
TraversalStepDTO,
|
|
23
|
+
)
|
|
24
|
+
from zeromodel.navigation.rules import TraversalRule
|
|
25
|
+
from zeromodel.navigation.traversal import replay_traversal, traverse
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"HierarchyCompilerSpecDTO",
|
|
29
|
+
"HierarchyManifestDTO",
|
|
30
|
+
"NavigationTileDTO",
|
|
31
|
+
"TraversalRequestDTO",
|
|
32
|
+
"TraversalResultDTO",
|
|
33
|
+
"TraversalReceiptDTO",
|
|
34
|
+
"TraversalRule",
|
|
35
|
+
"TraversalStepDTO",
|
|
36
|
+
"compile_hierarchy",
|
|
37
|
+
"replay_traversal",
|
|
38
|
+
"traverse",
|
|
39
|
+
"validate_hierarchy",
|
|
40
|
+
]
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Deterministic compilation and structural closure validation.
|
|
2
|
+
|
|
3
|
+
`compile_hierarchy` never reorders its input: identical `source_artifacts`
|
|
4
|
+
order plus an identical `spec` always yields an identical `HierarchyManifestDTO.
|
|
5
|
+
hierarchy_id`; a deliberately different order or a changed spec parameter
|
|
6
|
+
yields a different one. `validate_hierarchy` is an exhaustive, finite
|
|
7
|
+
structural walk of an already-compiled hierarchy - it performs no
|
|
8
|
+
similarity comparison and is not search.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import List, Optional, Sequence, Tuple
|
|
14
|
+
|
|
15
|
+
from zeromodel.artifacts import ArtifactRef, ArtifactResolver, ArtifactStore
|
|
16
|
+
|
|
17
|
+
from zeromodel.navigation.dto import (
|
|
18
|
+
HierarchyCompilerSpecDTO,
|
|
19
|
+
HierarchyManifestDTO,
|
|
20
|
+
NavigationTileDTO,
|
|
21
|
+
TileCoverageDTO,
|
|
22
|
+
TilePointerDTO,
|
|
23
|
+
compute_hierarchy_id,
|
|
24
|
+
compute_leaf_id,
|
|
25
|
+
compute_source_artifact_digest,
|
|
26
|
+
compute_tile_id,
|
|
27
|
+
)
|
|
28
|
+
from zeromodel.navigation.dto import LeafBindingDTO
|
|
29
|
+
from zeromodel.navigation.errors import HierarchyClosureError, HierarchyCompilationError
|
|
30
|
+
from zeromodel.navigation.storage import (
|
|
31
|
+
TILE_ARTIFACT_KIND,
|
|
32
|
+
leaf_binding_exists,
|
|
33
|
+
load_leaf_binding,
|
|
34
|
+
load_tile,
|
|
35
|
+
store_leaf_binding,
|
|
36
|
+
store_tile,
|
|
37
|
+
tile_exists,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _chunk(items: Sequence, size: int) -> List[Sequence]:
|
|
42
|
+
return [items[i : i + size] for i in range(0, len(items), size)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validate_corpus_contract(
|
|
46
|
+
spec: HierarchyCompilerSpecDTO, source_artifacts: Sequence[ArtifactRef]
|
|
47
|
+
) -> None:
|
|
48
|
+
if not source_artifacts:
|
|
49
|
+
raise HierarchyCompilationError(
|
|
50
|
+
"compile_hierarchy requires at least one source artifact"
|
|
51
|
+
)
|
|
52
|
+
seen: set = set()
|
|
53
|
+
for ref in source_artifacts:
|
|
54
|
+
if ref.artifact_kind != spec.corpus_artifact_kind:
|
|
55
|
+
raise HierarchyCompilationError(
|
|
56
|
+
f"artifact {ref.artifact_id} has kind {ref.artifact_kind!r}, "
|
|
57
|
+
f"but this corpus requires {spec.corpus_artifact_kind!r}"
|
|
58
|
+
)
|
|
59
|
+
key = (ref.artifact_kind, ref.artifact_id)
|
|
60
|
+
if key in seen:
|
|
61
|
+
raise HierarchyCompilationError(
|
|
62
|
+
f"duplicate source artifact {ref.artifact_id}: each source artifact "
|
|
63
|
+
"must appear at most once in one hierarchy"
|
|
64
|
+
)
|
|
65
|
+
seen.add(key)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_leaf_level(
|
|
69
|
+
spec: HierarchyCompilerSpecDTO,
|
|
70
|
+
source_artifacts: Sequence[ArtifactRef],
|
|
71
|
+
store: ArtifactStore,
|
|
72
|
+
) -> List[Tuple[TilePointerDTO, int]]:
|
|
73
|
+
"""Level 0: one leaf binding per source artifact, in declared order.
|
|
74
|
+
|
|
75
|
+
Each entry is (pointer, leaf_count_under_this_pointer) - leaf_count is 1
|
|
76
|
+
for every leaf and is summed up through parent levels for coverage.
|
|
77
|
+
"""
|
|
78
|
+
level: List[Tuple[TilePointerDTO, int]] = []
|
|
79
|
+
for index, ref in enumerate(source_artifacts):
|
|
80
|
+
leaf_id = compute_leaf_id(artifact_ref=ref, leaf_semantics=spec.leaf_semantics)
|
|
81
|
+
binding = LeafBindingDTO(
|
|
82
|
+
leaf_id=leaf_id, artifact_ref=ref, leaf_semantics=spec.leaf_semantics
|
|
83
|
+
)
|
|
84
|
+
store_leaf_binding(store, binding)
|
|
85
|
+
pointer = TilePointerDTO(
|
|
86
|
+
pointer_kind="leaf", target_id=leaf_id, order_key=f"{index:08d}"
|
|
87
|
+
)
|
|
88
|
+
level.append((pointer, 1))
|
|
89
|
+
return level
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_parent_level(
|
|
93
|
+
current: List[Tuple[TilePointerDTO, int]],
|
|
94
|
+
spec: HierarchyCompilerSpecDTO,
|
|
95
|
+
depth_from_leaves: int,
|
|
96
|
+
store: ArtifactStore,
|
|
97
|
+
) -> List[Tuple[TilePointerDTO, int]]:
|
|
98
|
+
next_level: List[Tuple[TilePointerDTO, int]] = []
|
|
99
|
+
for chunk_index, chunk_entries in enumerate(
|
|
100
|
+
_chunk(current, spec.max_children_per_tile)
|
|
101
|
+
):
|
|
102
|
+
children = tuple(entry[0] for entry in chunk_entries)
|
|
103
|
+
leaf_count = sum(entry[1] for entry in chunk_entries)
|
|
104
|
+
coverage = TileCoverageDTO(
|
|
105
|
+
corpus_id=spec.corpus_id,
|
|
106
|
+
partition_key=f"L{depth_from_leaves}-{chunk_index}",
|
|
107
|
+
child_count=len(children),
|
|
108
|
+
leaf_count=leaf_count,
|
|
109
|
+
)
|
|
110
|
+
tile_id = compute_tile_id(
|
|
111
|
+
depth=depth_from_leaves,
|
|
112
|
+
coverage=coverage,
|
|
113
|
+
children=children,
|
|
114
|
+
tie_rule=spec.tie_rule,
|
|
115
|
+
)
|
|
116
|
+
tile = NavigationTileDTO(
|
|
117
|
+
tile_id=tile_id,
|
|
118
|
+
depth=depth_from_leaves,
|
|
119
|
+
coverage=coverage,
|
|
120
|
+
children=children,
|
|
121
|
+
tie_rule=spec.tie_rule,
|
|
122
|
+
)
|
|
123
|
+
store_tile(store, tile)
|
|
124
|
+
pointer = TilePointerDTO(
|
|
125
|
+
pointer_kind="tile", target_id=tile_id, order_key=f"{chunk_index:08d}"
|
|
126
|
+
)
|
|
127
|
+
next_level.append((pointer, leaf_count))
|
|
128
|
+
return next_level
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _build_manifest(
|
|
132
|
+
spec: HierarchyCompilerSpecDTO, root_ref: ArtifactRef, source_artifact_digest: str
|
|
133
|
+
) -> HierarchyManifestDTO:
|
|
134
|
+
hierarchy_id = compute_hierarchy_id(
|
|
135
|
+
root_ref=root_ref, source_artifact_digest=source_artifact_digest, spec=spec
|
|
136
|
+
)
|
|
137
|
+
return HierarchyManifestDTO(
|
|
138
|
+
hierarchy_id=hierarchy_id,
|
|
139
|
+
root_ref=root_ref,
|
|
140
|
+
source_artifact_digest=source_artifact_digest,
|
|
141
|
+
compiler_id=spec.compiler_id,
|
|
142
|
+
compiler_version=spec.compiler_version,
|
|
143
|
+
corpus_id=spec.corpus_id,
|
|
144
|
+
corpus_artifact_kind=spec.corpus_artifact_kind,
|
|
145
|
+
leaf_semantics=spec.leaf_semantics,
|
|
146
|
+
max_children_per_tile=spec.max_children_per_tile,
|
|
147
|
+
max_depth=spec.max_depth,
|
|
148
|
+
tie_rule=spec.tie_rule,
|
|
149
|
+
failure_rule=spec.failure_rule,
|
|
150
|
+
navigation_rule_contract=spec.navigation_rule_contract,
|
|
151
|
+
child_ordering_rule=spec.child_ordering_rule,
|
|
152
|
+
partition_parameters=spec.partition_parameters,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def compile_hierarchy(
|
|
157
|
+
*,
|
|
158
|
+
spec: HierarchyCompilerSpecDTO,
|
|
159
|
+
source_artifacts: Sequence[ArtifactRef],
|
|
160
|
+
store: ArtifactStore,
|
|
161
|
+
) -> HierarchyManifestDTO:
|
|
162
|
+
_validate_corpus_contract(spec, source_artifacts)
|
|
163
|
+
|
|
164
|
+
current = _build_leaf_level(spec, source_artifacts, store)
|
|
165
|
+
depth_from_leaves = 0
|
|
166
|
+
while True:
|
|
167
|
+
next_level = _build_parent_level(current, spec, depth_from_leaves, store)
|
|
168
|
+
if len(next_level) == 1:
|
|
169
|
+
root_ref = ArtifactRef(
|
|
170
|
+
artifact_kind="navigation-tile", artifact_id=next_level[0][0].target_id
|
|
171
|
+
)
|
|
172
|
+
break
|
|
173
|
+
current = next_level
|
|
174
|
+
depth_from_leaves += 1
|
|
175
|
+
|
|
176
|
+
source_artifact_digest = compute_source_artifact_digest(tuple(source_artifacts))
|
|
177
|
+
manifest = _build_manifest(spec, root_ref, source_artifact_digest)
|
|
178
|
+
|
|
179
|
+
# Defense in depth: the compiler never hands back a hierarchy that
|
|
180
|
+
# would not itself pass closure validation (this also enforces
|
|
181
|
+
# max_depth against the tree the grouping loop actually produced).
|
|
182
|
+
try:
|
|
183
|
+
validate_hierarchy(manifest, store)
|
|
184
|
+
except HierarchyClosureError as exc:
|
|
185
|
+
raise HierarchyCompilationError(
|
|
186
|
+
f"compiled hierarchy failed closure validation: {exc}"
|
|
187
|
+
) from exc
|
|
188
|
+
return manifest
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _validate_root_reference(
|
|
192
|
+
manifest: HierarchyManifestDTO, store: ArtifactResolver
|
|
193
|
+
) -> None:
|
|
194
|
+
if manifest.root_ref.artifact_kind != TILE_ARTIFACT_KIND:
|
|
195
|
+
raise HierarchyClosureError(
|
|
196
|
+
f"hierarchy root reference must have kind {TILE_ARTIFACT_KIND!r}, "
|
|
197
|
+
f"got {manifest.root_ref.artifact_kind!r}"
|
|
198
|
+
)
|
|
199
|
+
if not tile_exists(store, manifest.root_ref.artifact_id):
|
|
200
|
+
raise HierarchyClosureError(
|
|
201
|
+
f"hierarchy root {manifest.root_ref.artifact_id} does not resolve"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _validate_tile_invariants(
|
|
206
|
+
tile: NavigationTileDTO,
|
|
207
|
+
tile_id: str,
|
|
208
|
+
manifest: HierarchyManifestDTO,
|
|
209
|
+
expected_depth_from_leaves: Optional[int],
|
|
210
|
+
) -> None:
|
|
211
|
+
if (
|
|
212
|
+
expected_depth_from_leaves is not None
|
|
213
|
+
and tile.depth != expected_depth_from_leaves
|
|
214
|
+
):
|
|
215
|
+
raise HierarchyClosureError(
|
|
216
|
+
f"tile {tile_id} declares depth={tile.depth}, but its position in the "
|
|
217
|
+
f"hierarchy requires depth={expected_depth_from_leaves}"
|
|
218
|
+
)
|
|
219
|
+
if tile.coverage.corpus_id != manifest.corpus_id:
|
|
220
|
+
raise HierarchyClosureError(
|
|
221
|
+
f"tile {tile_id} declares corpus_id={tile.coverage.corpus_id!r}, "
|
|
222
|
+
f"expected {manifest.corpus_id!r}"
|
|
223
|
+
)
|
|
224
|
+
if tile.coverage.child_count != len(tile.children):
|
|
225
|
+
raise HierarchyClosureError(
|
|
226
|
+
f"tile {tile_id} declares child_count that does not match its children"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _validate_leaf_child(
|
|
231
|
+
*,
|
|
232
|
+
store: ArtifactResolver,
|
|
233
|
+
manifest: HierarchyManifestDTO,
|
|
234
|
+
tile_id: str,
|
|
235
|
+
tile: NavigationTileDTO,
|
|
236
|
+
child: TilePointerDTO,
|
|
237
|
+
) -> ArtifactRef:
|
|
238
|
+
"""Validate one leaf child and return its resolved source artifact ref."""
|
|
239
|
+
if tile.depth != 0:
|
|
240
|
+
raise HierarchyClosureError(
|
|
241
|
+
f"tile {tile_id} has a leaf child but declares depth={tile.depth} (expected 0)"
|
|
242
|
+
)
|
|
243
|
+
if not leaf_binding_exists(store, child.target_id):
|
|
244
|
+
raise HierarchyClosureError(
|
|
245
|
+
f"tile {tile_id} child leaf {child.target_id} does not resolve"
|
|
246
|
+
)
|
|
247
|
+
binding = load_leaf_binding(store, child.target_id)
|
|
248
|
+
if binding.leaf_semantics != manifest.leaf_semantics:
|
|
249
|
+
raise HierarchyClosureError(
|
|
250
|
+
f"leaf {child.target_id} declares semantics {binding.leaf_semantics!r}, "
|
|
251
|
+
f"expected {manifest.leaf_semantics!r}"
|
|
252
|
+
)
|
|
253
|
+
if binding.artifact_ref.artifact_kind != manifest.corpus_artifact_kind:
|
|
254
|
+
raise HierarchyClosureError(
|
|
255
|
+
f"leaf {child.target_id} artifact kind {binding.artifact_ref.artifact_kind!r} "
|
|
256
|
+
f"violates corpus contract {manifest.corpus_artifact_kind!r}"
|
|
257
|
+
)
|
|
258
|
+
if not store.has(binding.artifact_ref):
|
|
259
|
+
raise HierarchyClosureError(
|
|
260
|
+
f"leaf {child.target_id} references source artifact "
|
|
261
|
+
f"{binding.artifact_ref.artifact_id} which does not resolve - "
|
|
262
|
+
"closure over the routing structure is not closure over the corpus"
|
|
263
|
+
)
|
|
264
|
+
return binding.artifact_ref
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _reconcile_source_digest(
|
|
268
|
+
manifest: HierarchyManifestDTO, reachable_source_refs: List[ArtifactRef]
|
|
269
|
+
) -> None:
|
|
270
|
+
actual_source_digest = compute_source_artifact_digest(tuple(reachable_source_refs))
|
|
271
|
+
if actual_source_digest != manifest.source_artifact_digest:
|
|
272
|
+
raise HierarchyClosureError(
|
|
273
|
+
"the hierarchy's actually-reachable source artifacts do not match its "
|
|
274
|
+
"declared source_artifact_digest - a source artifact was added, removed, "
|
|
275
|
+
"or substituted since compilation"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def validate_hierarchy(
|
|
280
|
+
manifest: HierarchyManifestDTO, store: ArtifactResolver
|
|
281
|
+
) -> Tuple[str, ...]:
|
|
282
|
+
"""Exhaustively validate structural closure. Returns every reachable
|
|
283
|
+
tile_id (root first) on success; raises `HierarchyClosureError` on the
|
|
284
|
+
first violation found.
|
|
285
|
+
|
|
286
|
+
This checks closure over the *routing structure* (tiles/leaf bindings
|
|
287
|
+
resolve, no cycles, bounded depth, consistent coverage/depth bookkeeping)
|
|
288
|
+
and over the *represented corpus* (every leaf's actual source artifact
|
|
289
|
+
resolves through the store, and the full reachable source set matches
|
|
290
|
+
the hierarchy's declared `source_artifact_digest` exactly - nothing
|
|
291
|
+
silently added, removed, or substituted).
|
|
292
|
+
"""
|
|
293
|
+
_validate_root_reference(manifest, store)
|
|
294
|
+
|
|
295
|
+
visited_tiles: List[str] = []
|
|
296
|
+
path_stack: set = set()
|
|
297
|
+
reachable_source_refs: List[ArtifactRef] = []
|
|
298
|
+
|
|
299
|
+
def _walk(
|
|
300
|
+
tile_id: str, depth_from_root: int, expected_depth_from_leaves: Optional[int]
|
|
301
|
+
) -> int:
|
|
302
|
+
"""Returns the number of leaves actually reachable under this tile."""
|
|
303
|
+
if tile_id in path_stack:
|
|
304
|
+
raise HierarchyClosureError(f"cycle detected at tile {tile_id}")
|
|
305
|
+
if depth_from_root > manifest.max_depth:
|
|
306
|
+
raise HierarchyClosureError(
|
|
307
|
+
f"hierarchy exceeds declared max_depth={manifest.max_depth} at tile {tile_id}"
|
|
308
|
+
)
|
|
309
|
+
tile = load_tile(store, tile_id)
|
|
310
|
+
_validate_tile_invariants(tile, tile_id, manifest, expected_depth_from_leaves)
|
|
311
|
+
visited_tiles.append(tile_id)
|
|
312
|
+
path_stack.add(tile_id)
|
|
313
|
+
|
|
314
|
+
actual_leaf_count = 0
|
|
315
|
+
seen_targets = set()
|
|
316
|
+
for child in tile.children:
|
|
317
|
+
if child.target_id == tile_id:
|
|
318
|
+
raise HierarchyClosureError(
|
|
319
|
+
f"tile {tile_id} references itself as a child"
|
|
320
|
+
)
|
|
321
|
+
if child.target_id in seen_targets:
|
|
322
|
+
raise HierarchyClosureError(
|
|
323
|
+
f"tile {tile_id} has a duplicate child reference {child.target_id}"
|
|
324
|
+
)
|
|
325
|
+
seen_targets.add(child.target_id)
|
|
326
|
+
|
|
327
|
+
if child.pointer_kind == "tile":
|
|
328
|
+
if not tile_exists(store, child.target_id):
|
|
329
|
+
raise HierarchyClosureError(
|
|
330
|
+
f"tile {tile_id} child tile {child.target_id} does not resolve"
|
|
331
|
+
)
|
|
332
|
+
actual_leaf_count += _walk(
|
|
333
|
+
child.target_id, depth_from_root + 1, tile.depth - 1
|
|
334
|
+
)
|
|
335
|
+
elif child.pointer_kind == "leaf":
|
|
336
|
+
reachable_source_refs.append(
|
|
337
|
+
_validate_leaf_child(
|
|
338
|
+
store=store,
|
|
339
|
+
manifest=manifest,
|
|
340
|
+
tile_id=tile_id,
|
|
341
|
+
tile=tile,
|
|
342
|
+
child=child,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
actual_leaf_count += 1
|
|
346
|
+
else: # pragma: no cover - TilePointerDTO already restricts this
|
|
347
|
+
raise HierarchyClosureError(
|
|
348
|
+
f"tile {tile_id} has a child with an unknown pointer_kind"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
path_stack.discard(tile_id)
|
|
352
|
+
|
|
353
|
+
if actual_leaf_count != tile.coverage.leaf_count:
|
|
354
|
+
raise HierarchyClosureError(
|
|
355
|
+
f"tile {tile_id} declares leaf_count={tile.coverage.leaf_count}, but "
|
|
356
|
+
f"{actual_leaf_count} leaves are actually reachable beneath it"
|
|
357
|
+
)
|
|
358
|
+
return actual_leaf_count
|
|
359
|
+
|
|
360
|
+
_walk(manifest.root_ref.artifact_id, 0, expected_depth_from_leaves=None)
|
|
361
|
+
_reconcile_source_digest(manifest, reachable_source_refs)
|
|
362
|
+
return tuple(visited_tiles)
|