zeromodel-navigation 1.1.0__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.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: zeromodel-navigation
3
+ Version: 1.1.0
4
+ Summary: ZeroModel finite, deterministic artifact-corpus hierarchy compiler and traversal engine
5
+ Author-email: Ernan Hughes <ernanhughes@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ernanhughes/zeromodel
8
+ Project-URL: Repository, https://github.com/ernanhughes/zeromodel
9
+ Project-URL: Documentation, https://ernanhughes.github.io/zeromodel/
10
+ Keywords: visual-policy-map,vpm,hierarchy,traversal,navigation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: numpy>=1.23
25
+ Requires-Dist: zeromodel==1.1.0
26
+ Requires-Dist: zeromodel-artifacts==1.1.0
27
+
28
+ # zeromodel-navigation
29
+
30
+ A finite, deterministic hierarchy compiler and traversal engine over
31
+ identified artifacts.
32
+
33
+ ## This is not Search
34
+
35
+ This package compiles and traverses a **finite, closed** hierarchy of
36
+ already-identified artifacts. It does not define similarity, relevance, or
37
+ nearest-neighbour retrieval. Traversal rules are stable, declared
38
+ descriptors with production implementations that route on explicit,
39
+ declared criteria (a request attribute, a declared priority, a fixed
40
+ target range) - never a learned or heuristic "closest match."
41
+
42
+ A later Search package may implement similarity-driven `TraversalRule`s
43
+ against the same `TraversalRule` protocol this package defines; that is out
44
+ of scope here.
45
+
46
+ ## Two separate hierarchy concepts
47
+
48
+ - The existing **intra-artifact pyramid** (`zeromodel.analysis.hierarchy`,
49
+ `build_pyramid`) reduces a single VPM field into coarser levels of
50
+ itself. Untouched by this package.
51
+ - The **artifact corpus hierarchy** this package compiles is a tree over
52
+ *many* identified artifacts: a root tile, internal navigation tiles, and
53
+ leaf bindings to individual artifacts.
54
+
55
+ ## Dependency rule
56
+
57
+ `navigation -> core + artifacts` only. Navigation does not depend on, or
58
+ import, Trust. Secure applications may compose Navigation and Trust
59
+ without coupling the packages directly - see the integration seam example
60
+ below.
61
+
62
+ ## Persistence
63
+
64
+ Navigation defines no store or repository of its own. Every tile and leaf
65
+ binding is persisted as canonical bytes through the Artifacts package's
66
+ `ArtifactStore`/`ArtifactResolver` protocol; Navigation owns the DTUs'
67
+ semantics, Artifacts owns storage and identity.
68
+
69
+ ## Public API
70
+
71
+ `HierarchyManifestDTO`, `NavigationTileDTO`, `HierarchyCompilerSpecDTO`,
72
+ `TraversalRule`, `TraversalRequestDTO`, `TraversalStepDTO`,
73
+ `TraversalResultDTO`, `TraversalReceiptDTO`, `compile_hierarchy`,
74
+ `validate_hierarchy`, `traverse`, `replay_traversal`.
75
+
76
+ Supporting DTOs (`TileCoverageDTO`, `TilePointerDTO`, `LeafBindingDTO`,
77
+ `TraversalRuleDescriptorDTO`, `TraversalFailureDTO`) are available from
78
+ `zeromodel.navigation.dto` for tests and advanced composition, but are not
79
+ part of the curated top-level surface.
80
+
81
+ ## Claims boundary
82
+
83
+ The supported claim is: compiling and deterministically traversing a
84
+ finite, identified hierarchy with complete artifact resolution and a
85
+ replayable trace.
86
+
87
+ This package does **not** claim: planet-scale hierarchies, infinite
88
+ in-memory capacity, logarithmic-time guarantees, semantic search,
89
+ nearest-neighbour retrieval, "40-hop world navigation," or
90
+ storage-independent performance.
91
+
92
+ ## Integration seam: composing with Trust (without depending on it)
93
+
94
+ Navigation never imports `zeromodel.trust`. A secure application composes
95
+ both packages at the call site:
96
+
97
+ ```python
98
+ from zeromodel.navigation import compile_hierarchy, validate_hierarchy, traverse
99
+ from zeromodel.trust import verify_artifact_for_scope, require_authorized
100
+
101
+ # 1. Resolve the hierarchy root (Navigation + Artifacts only).
102
+ root_manifest = ... # HierarchyManifestDTO, previously compiled
103
+
104
+ # 2. Verify the root/hierarchy is authorized before trusting its contents
105
+ # (Trust only - Navigation is not involved in this step).
106
+ decision = verify_artifact_for_scope(
107
+ artifact_ref=root_manifest.root_ref,
108
+ ...,
109
+ )
110
+ require_authorized(decision) # raises ArtifactNotAuthorized if not
111
+
112
+ # 3. Validate structural closure (Navigation only).
113
+ validate_hierarchy(root_manifest, store=artifact_store)
114
+
115
+ # 4. Traverse (Navigation only).
116
+ result = traverse(manifest=root_manifest, store=artifact_store, rule=rule, request=request)
117
+
118
+ # 5. Optionally, verify each resolved leaf artifact through Trust before use.
119
+ ```
120
+
121
+ This example is illustrative only - it does not execute a real controller
122
+ and makes no safety-certification claim. A safety-critical caller must not
123
+ skip step 2 or step 5.
@@ -0,0 +1,96 @@
1
+ # zeromodel-navigation
2
+
3
+ A finite, deterministic hierarchy compiler and traversal engine over
4
+ identified artifacts.
5
+
6
+ ## This is not Search
7
+
8
+ This package compiles and traverses a **finite, closed** hierarchy of
9
+ already-identified artifacts. It does not define similarity, relevance, or
10
+ nearest-neighbour retrieval. Traversal rules are stable, declared
11
+ descriptors with production implementations that route on explicit,
12
+ declared criteria (a request attribute, a declared priority, a fixed
13
+ target range) - never a learned or heuristic "closest match."
14
+
15
+ A later Search package may implement similarity-driven `TraversalRule`s
16
+ against the same `TraversalRule` protocol this package defines; that is out
17
+ of scope here.
18
+
19
+ ## Two separate hierarchy concepts
20
+
21
+ - The existing **intra-artifact pyramid** (`zeromodel.analysis.hierarchy`,
22
+ `build_pyramid`) reduces a single VPM field into coarser levels of
23
+ itself. Untouched by this package.
24
+ - The **artifact corpus hierarchy** this package compiles is a tree over
25
+ *many* identified artifacts: a root tile, internal navigation tiles, and
26
+ leaf bindings to individual artifacts.
27
+
28
+ ## Dependency rule
29
+
30
+ `navigation -> core + artifacts` only. Navigation does not depend on, or
31
+ import, Trust. Secure applications may compose Navigation and Trust
32
+ without coupling the packages directly - see the integration seam example
33
+ below.
34
+
35
+ ## Persistence
36
+
37
+ Navigation defines no store or repository of its own. Every tile and leaf
38
+ binding is persisted as canonical bytes through the Artifacts package's
39
+ `ArtifactStore`/`ArtifactResolver` protocol; Navigation owns the DTUs'
40
+ semantics, Artifacts owns storage and identity.
41
+
42
+ ## Public API
43
+
44
+ `HierarchyManifestDTO`, `NavigationTileDTO`, `HierarchyCompilerSpecDTO`,
45
+ `TraversalRule`, `TraversalRequestDTO`, `TraversalStepDTO`,
46
+ `TraversalResultDTO`, `TraversalReceiptDTO`, `compile_hierarchy`,
47
+ `validate_hierarchy`, `traverse`, `replay_traversal`.
48
+
49
+ Supporting DTOs (`TileCoverageDTO`, `TilePointerDTO`, `LeafBindingDTO`,
50
+ `TraversalRuleDescriptorDTO`, `TraversalFailureDTO`) are available from
51
+ `zeromodel.navigation.dto` for tests and advanced composition, but are not
52
+ part of the curated top-level surface.
53
+
54
+ ## Claims boundary
55
+
56
+ The supported claim is: compiling and deterministically traversing a
57
+ finite, identified hierarchy with complete artifact resolution and a
58
+ replayable trace.
59
+
60
+ This package does **not** claim: planet-scale hierarchies, infinite
61
+ in-memory capacity, logarithmic-time guarantees, semantic search,
62
+ nearest-neighbour retrieval, "40-hop world navigation," or
63
+ storage-independent performance.
64
+
65
+ ## Integration seam: composing with Trust (without depending on it)
66
+
67
+ Navigation never imports `zeromodel.trust`. A secure application composes
68
+ both packages at the call site:
69
+
70
+ ```python
71
+ from zeromodel.navigation import compile_hierarchy, validate_hierarchy, traverse
72
+ from zeromodel.trust import verify_artifact_for_scope, require_authorized
73
+
74
+ # 1. Resolve the hierarchy root (Navigation + Artifacts only).
75
+ root_manifest = ... # HierarchyManifestDTO, previously compiled
76
+
77
+ # 2. Verify the root/hierarchy is authorized before trusting its contents
78
+ # (Trust only - Navigation is not involved in this step).
79
+ decision = verify_artifact_for_scope(
80
+ artifact_ref=root_manifest.root_ref,
81
+ ...,
82
+ )
83
+ require_authorized(decision) # raises ArtifactNotAuthorized if not
84
+
85
+ # 3. Validate structural closure (Navigation only).
86
+ validate_hierarchy(root_manifest, store=artifact_store)
87
+
88
+ # 4. Traverse (Navigation only).
89
+ result = traverse(manifest=root_manifest, store=artifact_store, rule=rule, request=request)
90
+
91
+ # 5. Optionally, verify each resolved leaf artifact through Trust before use.
92
+ ```
93
+
94
+ This example is illustrative only - it does not execute a real controller
95
+ and makes no safety-certification claim. A safety-critical caller must not
96
+ skip step 2 or step 5.
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zeromodel-navigation"
7
+ version = "1.1.0"
8
+ description = "ZeroModel finite, deterministic artifact-corpus hierarchy compiler and traversal engine"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Ernan Hughes", email = "ernanhughes@gmail.com"}]
13
+ keywords = [
14
+ "visual-policy-map",
15
+ "vpm",
16
+ "hierarchy",
17
+ "traversal",
18
+ "navigation",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "Intended Audience :: Science/Research",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ ]
33
+ dependencies = [
34
+ "numpy>=1.23",
35
+ "zeromodel==1.1.0",
36
+ "zeromodel-artifacts==1.1.0",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/ernanhughes/zeromodel"
41
+ Repository = "https://github.com/ernanhughes/zeromodel"
42
+ Documentation = "https://ernanhughes.github.io/zeromodel/"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["src"]
46
+ include = ["zeromodel.navigation*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)