myko/core/report/export_tree.rs
1//! Entity tree export report.
2//!
3//! BFS walks the relationship graph from a root entity, collecting all
4//! descendant entities into a flat `EntityTreeExport` structure.
5
6use std::{
7 collections::{HashMap, HashSet, VecDeque},
8 sync::Arc,
9};
10
11use chrono::Utc;
12use hyphae::{Cell, Definite, Materialize};
13use myko_macros::{myko_report, myko_report_output};
14use serde_json::Value;
15
16use crate::core::capability::{RegistryScoped, Replaying};
17// The BFS traversal + report handler are server-only (they pull in hyphae and
18// the store registry); their imports are gated alongside them below.
19use crate::{
20 common::to_value::ToValue,
21 relationship::{Relation, iter_relations},
22 store::StoreRegistry,
23};
24
25// ─────────────────────────────────────────────────────────────────────────────
26// Output types
27// ─────────────────────────────────────────────────────────────────────────────
28
29/// A single exported entity within the tree.
30#[myko_report_output]
31#[derive(Eq)]
32pub struct ExportedEntity {
33 /// The entity type name (e.g., "Scene", "Binding").
34 pub entity_type: Arc<str>,
35 /// The full serialized entity data.
36 pub data: Value,
37}
38
39/// The complete tree export containing all entities reachable from the root.
40#[myko_report_output]
41#[derive(Eq)]
42pub struct EntityTreeExport {
43 /// Export format version.
44 pub version: u32,
45 /// Entity type of the root (e.g., "Project").
46 pub root_type: Arc<str>,
47 /// ID of the root entity.
48 pub root_id: Arc<str>,
49 /// ISO 8601 timestamp of when the export was created.
50 pub exported_at: String,
51 /// All entities in the tree, flattened.
52 pub entities: Vec<ExportedEntity>,
53}
54
55// ─────────────────────────────────────────────────────────────────────────────
56// Report definition
57// ─────────────────────────────────────────────────────────────────────────────
58
59/// Export the full entity tree rooted at a given entity.
60///
61/// Performs a BFS walk over the relationship graph, collecting every
62/// reachable descendant. Respects `exclude_from_tree` on `BelongsTo`
63/// relations and follows `EnsureFor` on a single-axis basis to prevent
64/// Cartesian explosion.
65#[myko_report(EntityTreeExport)]
66pub struct ExportEntityTree {
67 /// Entity type of the root (e.g., "Project").
68 pub root_type: Arc<str>,
69 /// ID of the root entity.
70 pub root_id: Arc<str>,
71 /// ISO 8601 timestamp — when set, replays events up to this time
72 /// into a temporary store and exports from that instead of the live store.
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 #[ts(optional = nullable)]
75 pub as_of: Option<Arc<str>>,
76}
77
78// ─────────────────────────────────────────────────────────────────────────────
79// Adjacency map
80// ─────────────────────────────────────────────────────────────────────────────
81
82/// Describes how to find children of a given parent entity type.
83pub struct ChildRelation {
84 /// The child entity type.
85 pub child_type: &'static str,
86 /// How to discover child IDs from the parent.
87 pub kind: ChildKind,
88}
89pub enum ChildKind {
90 /// `BelongsTo`: scan the child store for entities whose FK matches the parent ID.
91 BelongsTo {
92 extract_fk: crate::relationship::FkExtractor,
93 },
94 /// `OwnsMany`: extract child IDs directly from the parent entity.
95 OwnsMany {
96 extract_ids: crate::relationship::ArrayExtractor,
97 },
98 /// `EnsureFor`: scan the local (ensured) store for entities whose FK matches the parent ID.
99 EnsureFor {
100 extract_fk: crate::relationship::FkExtractor,
101 },
102}
103
104/// Build a map from parent entity type -> list of child relations.
105///
106/// This processes all registered relationships once and inverts them into
107/// a lookup table suitable for BFS traversal.
108#[must_use]
109pub fn build_adjacency_map() -> HashMap<&'static str, Vec<ChildRelation>> {
110 let mut map: HashMap<&'static str, Vec<ChildRelation>> = HashMap::new();
111
112 for reg in iter_relations() {
113 match ®.relation {
114 Relation::BelongsTo {
115 local_type,
116 foreign_type,
117 extract_fk,
118 exclude_from_tree,
119 ..
120 } => {
121 if *exclude_from_tree {
122 continue;
123 }
124 // Parent is foreign_type, child is local_type.
125 map.entry(foreign_type).or_default().push(ChildRelation {
126 child_type: local_type,
127 kind: ChildKind::BelongsTo {
128 extract_fk: *extract_fk,
129 },
130 });
131 }
132 Relation::OwnsMany {
133 local_type,
134 foreign_type,
135 extract_ids,
136 exclude_from_tree,
137 ..
138 } => {
139 if *exclude_from_tree {
140 continue;
141 }
142 // Parent is local_type, child is foreign_type.
143 map.entry(local_type).or_default().push(ChildRelation {
144 child_type: foreign_type,
145 kind: ChildKind::OwnsMany {
146 extract_ids: *extract_ids,
147 },
148 });
149 }
150 Relation::EnsureFor {
151 local_type,
152 dependencies,
153 exclude_from_tree,
154 ..
155 } => {
156 if *exclude_from_tree {
157 continue;
158 }
159 // For each dependency, the dependency's foreign_type is a parent
160 // that can reach local_type children (single-axis to avoid Cartesian).
161 for dep in *dependencies {
162 map.entry(dep.foreign_type)
163 .or_default()
164 .push(ChildRelation {
165 child_type: local_type,
166 kind: ChildKind::EnsureFor {
167 extract_fk: dep.extract_fk,
168 },
169 });
170 }
171 }
172 }
173 }
174
175 map
176}
177
178// ─────────────────────────────────────────────────────────────────────────────
179// BFS walk
180// ─────────────────────────────────────────────────────────────────────────────
181
182/// Walk the entity tree via BFS starting from `(root_type, root_id)`.
183///
184/// Returns all reachable entities (including the root) as `ExportedEntity` values.
185#[must_use]
186pub fn walk_tree<S>(
187 root_type: &str,
188 root_id: &str,
189 registry: &StoreRegistry,
190 adjacency: &HashMap<&'static str, Vec<ChildRelation>, S>,
191) -> Vec<ExportedEntity>
192where
193 S: std::hash::BuildHasher,
194{
195 let mut result = Vec::new();
196 let mut visited: HashSet<(Arc<str>, Arc<str>)> = HashSet::new();
197 let mut queue: VecDeque<(Arc<str>, Arc<str>)> = VecDeque::new();
198
199 let root_type: Arc<str> = root_type.into();
200 let root_id: Arc<str> = root_id.into();
201
202 queue.push_back((root_type.clone(), root_id.clone()));
203 visited.insert((root_type, root_id));
204
205 while let Some((entity_type, entity_id)) = queue.pop_front() {
206 // Fetch entity from store
207 let Some(store) = registry.get(&entity_type) else {
208 continue;
209 };
210 let Some(entity) = store.get_value(&entity_id) else {
211 continue;
212 };
213
214 // Serialize entity
215 result.push(ExportedEntity {
216 entity_type: entity_type.clone(),
217 data: entity.to_value(),
218 });
219
220 // Find children via adjacency map
221 let Some(children) = adjacency.get(entity_type.as_ref()) else {
222 continue;
223 };
224
225 for child_rel in children {
226 match &child_rel.kind {
227 ChildKind::BelongsTo { extract_fk } => {
228 // Scan child store for entities whose FK matches this entity's ID
229 let Some(child_store) = registry.get(child_rel.child_type) else {
230 continue;
231 };
232 for (child_id, child_item) in child_store.snapshot() {
233 if let Some(fk) = extract_fk(child_item.as_any())
234 && fk == entity_id
235 {
236 let key = (Arc::<str>::from(child_rel.child_type), child_id);
237 if visited.insert(key.clone()) {
238 queue.push_back(key);
239 }
240 }
241 }
242 }
243 ChildKind::OwnsMany { extract_ids } => {
244 // Extract child IDs directly from the parent entity
245 if let Some(ids) = extract_ids(entity.as_any()) {
246 for child_id in ids {
247 let key = (Arc::<str>::from(child_rel.child_type), child_id);
248 if visited.insert(key.clone()) {
249 queue.push_back(key);
250 }
251 }
252 }
253 }
254 ChildKind::EnsureFor { extract_fk } => {
255 // Scan the ensured entity store for entities whose FK matches this entity's ID
256 let Some(ensured_store) = registry.get(child_rel.child_type) else {
257 continue;
258 };
259 for (ensured_id, ensured_item) in ensured_store.snapshot() {
260 if let Some(fk) = extract_fk(ensured_item.as_any())
261 && fk == entity_id
262 {
263 let key = (Arc::<str>::from(child_rel.child_type), ensured_id);
264 if visited.insert(key.clone()) {
265 queue.push_back(key);
266 }
267 }
268 }
269 }
270 }
271 }
272 }
273
274 result
275}
276
277// ─────────────────────────────────────────────────────────────────────────────
278// ReportHandler impl
279// ─────────────────────────────────────────────────────────────────────────────
280
281// Handlers compile on every target so downstream entity crates do not need to
282// duplicate target gates. Report computation still runs server-side in practice.
283impl crate::report::ReportHandler for ExportEntityTree {
284 type Output = EntityTreeExport;
285
286 fn compute(
287 &self,
288 ctx: crate::report::ReportContext,
289 ) -> impl Materialize<Arc<Self::Output>, Definite> {
290 let registry = if let Some(as_of) = &self.as_of {
291 match ctx.replay_store(as_of) {
292 Ok(r) => r,
293 Err(err) => {
294 eprintln!("[ExportEntityTree] replay_store FAILED: as_of={as_of} err={err}");
295 return Cell::new(Arc::new(EntityTreeExport {
296 version: 1,
297 root_type: self.root_type.clone(),
298 root_id: self.root_id.clone(),
299 exported_at: Utc::now().to_rfc3339(),
300 entities: vec![],
301 }))
302 .lock();
303 }
304 }
305 } else {
306 ctx.registry()
307 };
308
309 eprintln!(
310 "[ExportEntityTree] registry has {} entity types, walking root_type={} root_id={}",
311 registry.entity_types().len(),
312 self.root_type,
313 self.root_id,
314 );
315 let adjacency = build_adjacency_map();
316 let entities = walk_tree(&self.root_type, &self.root_id, ®istry, &adjacency);
317 eprintln!(
318 "[ExportEntityTree] walk_tree found {} entities",
319 entities.len()
320 );
321
322 Cell::new(Arc::new(EntityTreeExport {
323 version: 1,
324 root_type: self.root_type.clone(),
325 root_id: self.root_id.clone(),
326 exported_at: Utc::now().to_rfc3339(),
327 entities,
328 }))
329 .lock()
330 }
331}