1#![recursion_limit = "256"]
2pub mod analysis_pipeline;
6pub mod analysis_publication;
7pub mod analytics;
8mod background_refresh;
9mod cli;
10pub mod config;
11pub(crate) mod corrections;
12pub mod dates;
13pub mod db;
14pub mod diagnostics;
15mod durable_fs;
16pub(crate) mod durable_path;
17mod executable_alias;
18pub mod export;
19pub mod files;
20mod fts;
21pub mod hashing;
22pub mod indexer;
23pub mod inspect;
24pub(crate) mod integrations;
25pub mod keymap;
26pub mod mcp_schema_budget;
27pub mod mcp_server;
28pub(crate) mod message_classification;
29pub mod message_search;
30mod message_search_batches;
31pub mod messages;
32pub mod migration;
33pub mod models;
34pub mod terminal_style;
35mod text_file_transaction;
36#[cfg_attr(not(test), warn(clippy::unwrap_used))]
41pub mod providers;
42pub mod refs;
43pub mod render;
44pub mod runtime;
45pub mod search_scope;
46pub mod service;
47pub mod skill_catalog;
48pub(crate) mod skill_manifest;
49pub mod skill_run;
50pub mod skills;
51pub mod source;
52mod sql_functions;
53pub mod sql_query;
54pub mod tail;
55pub mod trigram;
56pub mod trigram_index;
57mod tui;
58pub(crate) mod update;
59pub mod util;
60
61pub use analysis_pipeline::{
65 AnalysisPolicySpec, AnalysisResult, ClassificationRuleSpec, ClassificationTarget,
66 PhraseTextMode, PhraseVocabularyPolicySpec,
67};
68pub use analysis_publication::{
69 AnalysisPublicationFormat, AnalysisPublicationPlan, AnalysisPublicationReceipt,
70};
71pub use cli::run_from as run_cli_from;
72pub use corrections::{CapabilityReceipt, MessageClassificationReport};
73pub use export::{ExportFormat, ExportPublicationPlan};
74pub use inspect::{EvidenceWindow, InspectionOptions};
75pub use message_search::{
76 AdditionalFieldText, ContextWindow, CoordinateUnit, DetailLevel, FieldViewBudget,
77 FieldViewExtent, FuzzyQuery, JsonPointer, LineWindow, LiteralQuery, MatchViewBudget,
78 MatchWindow, MessageFieldView, MessageMatchEvidence, MessageMatchViewMarkers,
79 MessagePredicates, MessageQuery, MessageResultRef, MessageSearchError, MessageSearchHit,
80 MessageSearchInclude, MessageSearchIncludedData, MessageSearchOmission, MessageSearchParameter,
81 MessageSearchParameterDomain, MessageSearchParameterRegistry, MessageSearchParameterSpec,
82 MessageSearchRequest, MessageSearchRequestBuilder, MessageSearchResponse, MessageSearchRule,
83 MessageSearchRuntimeDiagnostics, MessageSearchSpecification, MessageTarget, PageSide,
84 ProviderScope, PurposeSelection, ReceiptLevel, RequestedExtent, RequestedTimeRange,
85 ResolvedMessageSearchRequest, ResolvedQueryMode, ResolvedRequestExtent,
86 ResolvedRequestPresentation, ResultSetExtent, SequenceRange, ValidatedRegex, ValueOriginKind,
87 ViewCharRange,
88};
89pub use message_search_batches::{
90 MessageSearchBatch, MessageSearchBatches, MessageSearchCompletion,
91};
92pub use models::{AnalysisRequest, AnalysisSessionSelection};
93pub use models::{
94 FileQuery, MessageClassificationMatch, MessageFilters, MessageSearchMode, SearchFilters,
95 SessionKind,
96};
97pub use search_scope::{
98 AccessRoot, AccessRootOrigin, AccessRootSource, EffectiveAccessScope, TrustedAccessInputs,
99};
100pub use service::SessionSearch;
101pub use service::{AnalysisReceipt, ReceiptedAnalysis};
102pub use skill_catalog::{SkillName, SkillNameSelector, SkillPathSelector, SkillSelector};
103pub use skill_run::{
104 CapabilityExecutionSource, MessageClassificationDefinition, MessageClassificationQuery,
105 MessageClassificationResult, ResolvedSkillReceipt, SelectedSkillLocation, SkillCapabilityInput,
106 SkillCapabilityOutput, SkillRunQuery, SkillRunReport,
107};
108
109#[doc(hidden)]
115pub fn run_cli_from_python<I, T>(args: I) -> anyhow::Result<i32>
116where
117 I: IntoIterator<Item = T>,
118 T: Into<std::ffi::OsString> + Clone,
119{
120 update::mark_python_binding_process();
121 cli::run_from(args)
122}
123
124#[doc(hidden)]
130pub fn is_broken_pipe_error(error: &anyhow::Error) -> bool {
131 error.chain().any(|cause| {
132 cause
133 .downcast_ref::<std::io::Error>()
134 .is_some_and(|error| error.kind() == std::io::ErrorKind::BrokenPipe)
135 || cause
136 .downcast_ref::<serde_json::Error>()
137 .is_some_and(|error| error.io_error_kind() == Some(std::io::ErrorKind::BrokenPipe))
138 })
139}
140
141#[doc(hidden)]
154pub fn is_storage_full_error(error: &anyhow::Error) -> bool {
155 error.chain().any(|cause| {
156 cause
157 .downcast_ref::<std::io::Error>()
158 .is_some_and(|error| error.kind() == std::io::ErrorKind::StorageFull)
159 || cause
160 .downcast_ref::<serde_json::Error>()
161 .is_some_and(|error| error.io_error_kind() == Some(std::io::ErrorKind::StorageFull))
162 || cause
163 .downcast_ref::<rusqlite::Error>()
164 .and_then(rusqlite::Error::sqlite_error_code)
165 .is_some_and(|code| code == rusqlite::ErrorCode::DiskFull)
166 })
167}
168
169const STORAGE_FULL_GUIDANCE: &str =
179 "There is no free space left for this write. No partial file replaced a complete one: file \
180 writes publish by atomic rename and database writes roll back, so the history already indexed \
181 is intact. Free space, then rerun; `aise doctor` reports index readiness.";
182
183#[doc(hidden)]
189pub fn error_message_with_recovery(error: &anyhow::Error) -> String {
190 let rendered = format!("{error:#}");
191 if is_storage_full_error(error) {
192 return format!("{rendered}\n{STORAGE_FULL_GUIDANCE}");
193 }
194 rendered
195}
196
197#[cfg(test)]
198mod storage_pressure_tests {
199 #[test]
206 fn storage_exhaustion_is_recognized_from_the_filesystem_and_from_sqlite() {
207 let staged = anyhow::Error::new(std::io::Error::from(std::io::ErrorKind::StorageFull))
208 .context("failed to write staging file /tmp/index.db.stage");
209 assert!(super::is_storage_full_error(&staged), "{staged:#}");
210
211 let database = anyhow::Error::new(rusqlite::Error::SqliteFailure(
212 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_FULL),
213 Some("database or disk is full".to_string()),
214 ))
215 .context("could not commit the index transaction");
216 assert!(super::is_storage_full_error(&database), "{database:#}");
217 }
218
219 #[test]
227 fn a_storage_failure_is_rendered_with_what_survived_and_what_to_do() {
228 let rendered = super::error_message_with_recovery(
229 &anyhow::Error::new(std::io::Error::from(std::io::ErrorKind::StorageFull))
230 .context("failed to write staging file"),
231 );
232 assert!(
233 rendered.contains("failed to write staging file"),
234 "{rendered}"
235 );
236 assert!(rendered.contains("no free space"), "{rendered}");
237 assert!(rendered.contains("is intact"), "{rendered}");
238 assert!(rendered.contains("aise doctor"), "{rendered}");
239 assert!(!rendered.contains("searchable"), "{rendered}");
242 }
243
244 #[test]
246 fn an_unrelated_failure_gains_no_storage_guidance() {
247 let error = anyhow::Error::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
248 .context("failed to write staging file");
249 assert_eq!(
250 super::error_message_with_recovery(&error),
251 format!("{error:#}")
252 );
253 }
254
255 #[test]
257 fn other_failures_are_not_reported_as_storage_exhaustion() {
258 for error in [
259 anyhow::Error::new(std::io::Error::from(std::io::ErrorKind::NotFound)),
260 anyhow::Error::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
261 anyhow::Error::new(rusqlite::Error::SqliteFailure(
262 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
263 Some("database is locked".to_string()),
264 )),
265 ] {
266 assert!(!super::is_storage_full_error(&error), "{error:#}");
267 }
268 }
269}