diff --git a/README.md b/README.md index 7c440af..107df9a 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ A built model can be saved to disk and loaded back. `load()` validates the M, M0 index.dump("model.bin")?; let loaded: Apotheosis = Apotheosis::load("model.bin")?; -// export::gexf::draw() writes model_layer0.gexf, model_layer1.gexf, ... next to the given base path. -apotheosis2::export::gexf::draw(&index, "model"); +// export::gexf::draw() also returns Result and writes model_layer0.gexf, model_layer1.gexf, ... +apotheosis2::export::gexf::draw(&index, "model")?; ``` diff --git a/docs/architecture/interfaz-apotheosis.md b/docs/architecture/interfaz-apotheosis.md index 434c49d..f6bfb65 100644 --- a/docs/architecture/interfaz-apotheosis.md +++ b/docs/architecture/interfaz-apotheosis.md @@ -207,7 +207,10 @@ The bound `where Self: serde::de::DeserializeOwned` imposes the same restriction ```rust // free function in the export::gexf module, not a facade method -pub fn draw>(model: &Apotheosis<...>, path: P) +pub fn draw>( + model: &Apotheosis<...>, + path: P, +) -> Result<(), Box> ``` **Semantics** @@ -218,7 +221,7 @@ Each GEXF file contains nodes (one per entry in that HNSW layer, identified by i **Error Handling** -Does not return `Result`. I/O errors when writing each GEXF file are silently discarded: the code uses `let _ = save_gexf(...)`. GEXF XML serialization errors (`gexf.to_string().unwrap()`) produce a panic. +Returns `Err` on I/O errors when writing a GEXF file and on GEXF XML serialization errors. The first failing layer aborts the export; files already written remain on disk. @@ -545,15 +548,15 @@ The crate does not adopt a uniform error-handling strategy: different methods us #### `Result<_, Box>` -Used by `dump`, `load`, and the `create` record constructors. The error type is a dynamic trait object that can wrap any I/O or serialization error. The caller must match on or propagate the error with `?`. There are no structured error types that would allow programmatic distinction between the possible causes (I/O failure, parameter mismatch, malformed file). +Used by `dump`, `load`, `export::gexf::draw`, and the `create` record constructors. The error type is a dynamic trait object that can wrap any I/O or serialization error. The caller must match on or propagate the error with `?`. There are no structured error types that would allow programmatic distinction between the possible causes (I/O failure, parameter mismatch, malformed file). #### Panics -`export::gexf::draw` can panic if the internal GEXF XML serialization fails (`gexf.to_string().unwrap()` in `save_gexf`). No public facade method has identifiable panics in the normal flow; the `create` record constructors return `Err` on malformed input instead of panicking. +No public method has identifiable panics in the normal flow: `export::gexf::draw` returns `Err` instead of panicking on serialization failures, and the `create` record constructors return `Err` on malformed input. #### Absence of error return -`insert` returns `bool` rather than `Result`: duplicate keys are signaled with `false` and a warning, without propagating an error to the caller. `export::gexf::draw` has no explicit error paths: I/O errors when writing each GEXF file are silently discarded via `let _ = save_gexf(...)`. +`insert` returns `bool` rather than `Result`: duplicate keys are signaled with `false` and a warning, without propagating an error to the caller. --- diff --git a/docs/architecture/views/cc/vista-cc.md b/docs/architecture/views/cc/vista-cc.md index 9c127c2..535397a 100644 --- a/docs/architecture/views/cc/vista-cc.md +++ b/docs/architecture/views/cc/vista-cc.md @@ -93,7 +93,7 @@ The course notes define *attachment* as the association between component ports | `apo↔radix-lookup` | `<>` | `Apotheosis::index-radix` | `RadixTree::lookup` | Input: `key: &[u8]` / Output: `Option<&RadixNode>` (Apotheosis extracts `Option` from `node.data`). If `Some(index)`: fast-path; if `None`: ANN path | First operation in every `search()` call. Its result determines which path is taken | | `apo→radix-insert` | `<>` | `Apotheosis::index-radix` | `RadixTree::insert` | Input: `(key: Vec, index: usize)` / no return | Executed on every new insertion, always alongside `hnsw-insert` | | `apo↔fs-io` | `<>` | `Apotheosis::persistence` | `FileSystem::io` | `dump(path)` writes to FS; `load(path)` reads from FS into Apotheosis | Bidirectional. `load` reconstructs the full model in memory | -| `apo→fs-gexf` | `<>` | `Apotheosis::persistence` | `FileSystem::gexf-out` | `export::gexf::draw(&model, path)` writes one `.gexf` file per HNSW layer with pattern `_layer.gexf` | Write only. No return value | +| `apo→fs-gexf` | `<>` | `Apotheosis::persistence` | `FileSystem::gexf-out` | `export::gexf::draw(&model, path)` writes one `.gexf` file per HNSW layer with pattern `_layer.gexf` | Write only. Returns `Result`: the first failed write or serialization aborts the export | | `fs→gephi` | `<>` | `FileSystem::gexf-out` | `Gephi::gexf-in` | `.gexf` file | Asynchronous with respect to APOTHEOSIS 2. Gephi opens the file independently; no runtime coupling | ### 2.3 Element Interfaces diff --git a/src/export/gexf.rs b/src/export/gexf.rs index 6d6bb37..a0631e1 100644 --- a/src/export/gexf.rs +++ b/src/export/gexf.rs @@ -20,7 +20,8 @@ use std::path::{Path, PathBuf}; pub fn draw( model: &Apotheosis, path: P, -) where +) -> Result<(), Box> +where R: ApotheosisRecord, D: DistanceAlgorithm + Default, P: AsRef, @@ -54,11 +55,17 @@ pub fn draw std::io::Result<()> { +fn save_gexf( + base_path: &Path, + layer_idx: usize, + gexf: &Gexf, +) -> Result<(), Box> { let mut file_path = PathBuf::from(base_path); if let Some(stem) = file_path.file_stem().and_then(|s| s.to_str()) { @@ -70,6 +77,6 @@ fn save_gexf(base_path: &Path, layer_idx: usize, gexf: &Gexf) -> std::io::Result file_path = PathBuf::from(filename); } - fs::write(file_path, gexf.to_string().unwrap())?; + fs::write(file_path, gexf.to_string()?)?; Ok(()) } diff --git a/tests/api_contract.rs b/tests/api_contract.rs index cd58b80..2e21ef2 100644 --- a/tests/api_contract.rs +++ b/tests/api_contract.rs @@ -76,7 +76,7 @@ fn draw_produces_one_gexf_file_per_layer() { } let layer_count = idx.draw_model().len(); - apotheosis2::export::gexf::draw(&idx, &base); + apotheosis2::export::gexf::draw(&idx, &base).expect("draw() must succeed for this dataset"); for n in 0..layer_count { let f = PathBuf::from(format!("{}/model_layer{}.gexf", dir, n));