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.
@@ -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)