fix(cli): close small JSON-contract gaps for agent consumers

- --offline now warns when it serves a cache entry past its TTL; before,
  arbitrarily old data was returned with no signal at all
- version, cache info and cache clear honour -o json
- ownership resolve map/merge honour -o json
- stocks quote/compare require at least one symbol instead of printing []
- cache schema-mismatch notice goes through runtime::info so --quiet
  silences it, instead of a raw debug eprintln

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Q2PufCUdvVZoVW1Dy53js
This commit is contained in:
Claude 2026-10-02 12:28:16 +00:00
commit 277273bc78
No known key found for this signature in database
6 changed files with 130 additions and 18 deletions

View file

@ -52,11 +52,7 @@ impl Cache {
let Some(entry): Option<CacheEntry<T>> = self.read_entry(data_type, symbol)? else {
return Ok(None);
};
let age = Utc::now().signed_duration_since(entry.fetched_at);
if age
>= chrono::Duration::from_std(Duration::from_secs(entry.ttl_secs))
.map_err(|e| IdxError::CacheMiss(e.to_string()))?
{
if is_expired(entry.fetched_at, entry.ttl_secs) {
return Ok(None);
}
Ok(Some(entry.data))
@ -70,6 +66,25 @@ impl Cache {
Ok(self.read_entry::<T>(data_type, symbol)?.map(|e| e.data))
}
/// Like [`Cache::get_stale`], but warns when the entry served is past its TTL.
/// Used by `--offline`, which otherwise serves arbitrarily old data silently.
pub fn get_offline<T: DeserializeOwned>(
&self,
data_type: &str,
symbol: &str,
) -> Result<Option<T>, IdxError> {
let Some(entry) = self.read_entry::<T>(data_type, symbol)? else {
return Ok(None);
};
if is_expired(entry.fetched_at, entry.ttl_secs) {
runtime::warn(format!(
"offline: serving expired cache for {symbol} (fetched {})",
entry.fetched_at.to_rfc3339()
));
}
Ok(Some(entry.data))
}
pub fn put<T: Serialize>(
&self,
data_type: &str,
@ -184,12 +199,12 @@ impl Cache {
}
};
if entry.schema_version != CURRENT_SCHEMA_VERSION {
eprintln!(
"debug: cache schema mismatch for {} (got {}, expected {})",
runtime::info(format!(
"cache schema mismatch for {} (got {}, expected {}), treating as miss",
path.display(),
entry.schema_version,
CURRENT_SCHEMA_VERSION
);
));
let _ = fs::remove_file(&path);
return Ok(None);
}
@ -201,6 +216,12 @@ impl Cache {
}
}
fn is_expired(fetched_at: DateTime<Utc>, ttl_secs: u64) -> bool {
let ttl =
chrono::Duration::from_std(Duration::from_secs(ttl_secs)).unwrap_or(chrono::Duration::MAX);
Utc::now().signed_duration_since(fetched_at) >= ttl
}
pub fn cache_dir() -> Result<PathBuf, IdxError> {
if let Ok(dir) = std::env::var("XDG_CACHE_HOME")
&& !dir.is_empty()