1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
//! Phase 8β T5 — Trait registry for cross-type queries via `#[djogi::trait_impl]`.
//!
//! # What
//!
//! [`TraitRegistration`] is the runtime entry that the
//! `#[djogi::trait_impl]` attribute macro emits via
//! `inventory::submit!` per `impl Trait for Model` block. The
//! registry lets cross-cutting consumers walk every model that
//! implements a given trait at runtime, without forcing every
//! adopter to enumerate the implementations by hand.
//!
//! # Why
//!
//! Adopters writing `#[djogi::trait_impl] impl Searchable for Vehicle
//! { ... }` get Vehicle visible to every cross-cutting query against
//! `dyn Searchable` without naming Vehicle in the consumer's path.
//!
//! Mirrors the established `inventory`-based registry pattern from
//! `djogi::apps::AppRegistry` — compile-time submission, one-shot
//! at first read, downstream consumers iterate.
//!
//! # Relationship to sassi's `Sassi::all_impl::<dyn T>()`
//!
//! Sassi ships its own `#[sassi::trait_impl]` attribute macro and
//! `inventory` registry (`sassi::trait_registry`). Sassi's registry
//! is keyed off `Punnu<T>`-scoped models — Sassi knows about
//! per-model pools and uses the registry to construct `Vec<Arc<dyn T>>`
//! across every Punnu-registered model implementing the trait.
//!
//! Djogi's registry here is a sibling surface for cross-type queries
//! that do **not** require the Punnu pool boundary. Adopters with
//! sassi enabled and Punnu-pooled models use `#[sassi::trait_impl]`
//! and `Sassi::all_impl::<dyn T>()` for the full sassi-integrated
//! cross-type query path; adopters who only need the descriptor-
//! level enumeration use `#[djogi::trait_impl]` and
//! [`iter_for_trait::<dyn T>()`] directly.
//!
//! Plan §7 #12 (resolved 2026-05-03): T5 owns `djogi::trait_registry::*`
//! surface only; the sassi-bridging consumer surface is sassi's own
//! responsibility. T7 (cluster 8δ) extends `djogi::cache::*` for the
//! Punnu cache surface; this module stays narrowly focused on the
//! registration plumbing.
//!
//! # Type erasure
//!
//! Trait-object pointer manipulation is the soundness-critical
//! surface. The registry's wire type is `Arc<dyn Any + Send + Sync>`;
//! the per-impl caster (T5.3) routes through a per-(Model, Trait)
//! carrier struct that satisfies `Any + Send + Sync + 'static`,
//! using only Rust's built-in coercions — `Arc::downcast`, unsized
//! `Arc<T>` → `Arc<dyn Trait>` coercion, never `transmute`.
/// Erased Arc carrier — `Arc<dyn Any + Send + Sync>`. Type alias
/// improves readability of the `caster` field signature and silences
/// the `clippy::type_complexity` lint.
pub type ErasedArc = Arc;
/// Type-erased caster function — accepts an `ErasedArc` holding the
/// model instance, returns `Some(carrier_for_dyn_T)` when the input
/// downcasts to the registered model type; `None` otherwise.
///
/// T5.3 finalises the body; T5.1 fixes the type alias so the
/// `TraitRegistration` field signature stays readable.
pub type CasterFn = fn ;
/// One trait-impl registration entry. Emitted via
/// `inventory::submit!(TraitRegistration { ... })` by the
/// `#[djogi::trait_impl]` attribute macro (T5.2 + T5.3).
///
/// # Fields
///
/// - `model_type_id` — the `TypeId` of the implementing model
/// (`Vehicle` for `impl Searchable for Vehicle`). Returned by a
/// `fn() -> TypeId` so the registration is `const`-constructible
/// at the macro emission site without touching `TypeId::of` (which
/// is `const`-only on nightly).
/// - `trait_type_id` — the `TypeId` of the registered trait
/// (`dyn Searchable`). Same `fn() -> TypeId` discipline.
/// - `model_type_name` / `trait_type_name` — human-readable names
/// for diagnostic / introspection paths. `&'static str` so the
/// registration stays `const`-submittable.
/// - `caster` — the type-erased downcast helper. T5.3 fills this in;
/// T5.1 ships the field shape only.
///
/// # Layout stability
///
/// Same convention as `ModelDescriptor` and the other inventory-
/// submitted descriptors: every text field is `&'static str`, every
/// non-text field is `Copy`, struct layout stays stable so adding a
/// future field is a breaking change at every emission site (which
/// is exactly the discipline we want — adopters never touch this
/// directly; the macro is the only emitter).
// Note: NOT `#[non_exhaustive]` — the macro-emitted code in adopter
// crates constructs this struct via a literal in `inventory::submit!`,
// which Rust rejects on `#[non_exhaustive]` structs (E0639). The
// add-a-field-is-breaking discipline is enforced by convention rather
// than the attribute; every macro emission site updates in lockstep
// when a new field lands.
collect!;
/// Convenience iterator over every registered `TraitRegistration`.
///
/// Wraps `inventory::iter::<TraitRegistration>` so consumers do not
/// need the `inventory` crate as a direct dependency. Prefer
/// [`iter_for_trait`] when filtering by a specific trait type id —
/// the unfiltered iterator is useful for diagnostic dumps.
/// One-shot, lazily-built index of `inventory::iter::<TraitRegistration>`
/// keyed by trait `TypeId`. Built on the first `iter_for_trait` call by
/// invoking each registration's `trait_type_id` fn-ptr exactly once;
/// every subsequent lookup is a single `HashMap::get`.
///
/// The pre-cache shape walked the full registration list and called
/// `(r.trait_type_id)()` on every entry per call — O(n) fn-ptr calls
/// per `iter_for_trait` invocation. The cache flips that to O(1) for
/// hot paths (e.g. cross-type queries that re-enumerate trait impls
/// frequently) at the cost of one map allocation per process.
static REGISTRY_CACHE: OnceLock = new;
/// Iterate every `TraitRegistration` whose `trait_type_id` matches
/// `T`'s `TypeId`. Phase 8β T5.4 — the consumer-side filter for
/// `Sassi::all_impl::<dyn T>()` and equivalent cross-type queries.
///
/// The `T: ?Sized + 'static` bound covers `dyn Trait` types — the
/// canonical caller-side spelling is
/// `iter_for_trait::<dyn Searchable>()`.
///
/// First call builds the keyed cache (O(n) fn-ptr calls); subsequent
/// calls are O(1) `HashMap` lookups. Returns `Vec<&'static …>` —
/// `inventory::collect!` already yields `'static` references, and the
/// cache holds them by reference, so the returned iterator borrows
/// from the static cache without per-call allocation aside from the
/// `Vec` clone of pointers (cheap; one word per registered impl).
Sized + 'static>