From aad29eaf23207dc7e2ef110ab9c2b63ad0471ccc Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Sun, 12 Apr 2026 14:27:47 +0700 Subject: [PATCH] feat: default ownership sync to published snapshot --- TODO.md | 1 + docs/OWNERSHIP_SYNC.md | 2 ++ src/cli/ownership.rs | 4 +-- src/ownership/snapshot.rs | 75 +++++++++++++++++++++++++++------------ 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/TODO.md b/TODO.md index 38eeb06..f884dca 100644 --- a/TODO.md +++ b/TODO.md @@ -163,6 +163,7 @@ - [ ] Additional providers (Alpha Vantage, Twelve Data, IDX official) ## 🔬 Latest Smoke Findings (2026-04-02) +- [x] `ownership sync` now falls back to the built-in published manifest URL on `2026-04-12`, so plain `idx ownership sync` works on a clean machine once the public snapshot release is available - [x] Ownership snapshot publishing now has a live-source maintainer path on `2026-04-11`: `scripts/build-latest-ownership-snapshot.sh` discovers the current supported `above1` IDX/KSEI PDF, imports it into an isolated temp DB, emits release-ready SQLite + manifest artifacts, and records the source IDX metadata in the manifest - [x] Verification on `2026-04-11`: `nix develop --command cargo test`, `bash -n scripts/build-latest-ownership-snapshot.sh`, and a real `nix develop --command scripts/build-latest-ownership-snapshot.sh --idx-bin ./target/debug/idx --output-dir tmp/ownership-snapshot-live` run all passed; the current live source produced `7214` rows for `955` tickers with `as_of_date=2026-03-31` - [x] Final release-hygiene pass on `2026-04-06`: crate metadata now declares `rust-version = 1.85`, README install docs now spell out Cargo helper-runtime expectations plus persistent `nix profile install`, and CI install smoke now runs the mock smoke matrix against the installed binary instead of only checking `idx version` diff --git a/docs/OWNERSHIP_SYNC.md b/docs/OWNERSHIP_SYNC.md index 0fd0a71..cb6d720 100644 --- a/docs/OWNERSHIP_SYNC.md +++ b/docs/OWNERSHIP_SYNC.md @@ -14,6 +14,8 @@ The manifest location is resolved in this order: 1. `idx ownership sync --manifest ` 2. `IDX_OWNERSHIP_SNAPSHOT_MANIFEST` 3. `ownership.snapshot_manifest` in `config.toml` +4. built-in published manifest URL: + `https://github.com/0xrsydn/idx-cli/releases/download/ownership-snapshot-current/ownership-snapshot-manifest.json` The value can be either: - a local path to a manifest JSON file diff --git a/src/cli/ownership.rs b/src/cli/ownership.rs index ccd9d30..4073649 100644 --- a/src/cli/ownership.rs +++ b/src/cli/ownership.rs @@ -47,7 +47,7 @@ pub enum OwnershipCommand { #[command( about = "Install or refresh a maintained ownership SQLite snapshot", long_about = "Install or refresh a maintained ownership SQLite snapshot.\n\nThis is the normal bootstrap/update path for ownership data.\n\nManifest lookup order:\n 1. `--manifest`\n 2. `IDX_OWNERSHIP_SNAPSHOT_MANIFEST`\n 3. `ownership.snapshot_manifest` in config", - after_help = "Examples:\n idx ownership sync\n idx ownership sync --manifest /path/to/ownership-snapshot-manifest.json\n IDX_OWNERSHIP_SNAPSHOT_MANIFEST=https://example.com/latest.json idx ownership sync" + after_help = "Examples:\n idx ownership sync\n idx ownership sync --manifest /path/to/ownership-snapshot-manifest.json\n IDX_OWNERSHIP_SNAPSHOT_MANIFEST=https://example.com/latest.json idx ownership sync\n\nIf none of the above are set, `ownership sync` falls back to the built-in published manifest URL." )] Sync(SyncArgs), /// Show all holders for a ticker (KSEI + Bing combined). @@ -100,7 +100,7 @@ pub struct ImportArgs { #[derive(Debug, Args)] pub struct SyncArgs { - /// Snapshot manifest location (URL or local path). If omitted, config/env lookup is used. + /// Snapshot manifest location (URL or local path). If omitted, idx falls back through env/config and then the published default URL. #[arg(long)] pub manifest: Option, /// Replace the local DB even when it is already current or newer than the snapshot. diff --git a/src/ownership/snapshot.rs b/src/ownership/snapshot.rs index e6cff7b..b0209b9 100644 --- a/src/ownership/snapshot.rs +++ b/src/ownership/snapshot.rs @@ -14,6 +14,7 @@ use crate::ownership::types::OwnershipRelease; pub const SNAPSHOT_MANIFEST_CONFIG_KEY: &str = "ownership.snapshot_manifest"; pub const SNAPSHOT_MANIFEST_ENV: &str = "IDX_OWNERSHIP_SNAPSHOT_MANIFEST"; pub const SNAPSHOT_MANIFEST_SCHEMA_VERSION: u32 = 1; +pub const DEFAULT_SNAPSHOT_MANIFEST_URL: &str = "https://github.com/0xrsydn/idx-cli/releases/download/ownership-snapshot-current/ownership-snapshot-manifest.json"; const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"; @@ -90,30 +91,27 @@ struct SyncDecision { } pub fn resolve_manifest_source(explicit: Option<&str>) -> Result { - if let Some(value) = explicit { - let trimmed = value.trim(); - if !trimmed.is_empty() { - return Ok(trimmed.to_string()); + let env_value = std::env::var(SNAPSHOT_MANIFEST_ENV).ok(); + let config_value = get_config_value(SNAPSHOT_MANIFEST_CONFIG_KEY)?; + + resolve_manifest_source_with(explicit, env_value.as_deref(), config_value.as_deref()) +} + +fn resolve_manifest_source_with( + explicit: Option<&str>, + env_value: Option<&str>, + config_value: Option<&str>, +) -> Result { + for value in [explicit, env_value, config_value] { + if let Some(value) = value { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Ok(trimmed.to_string()); + } } } - if let Ok(value) = std::env::var(SNAPSHOT_MANIFEST_ENV) { - let trimmed = value.trim(); - if !trimmed.is_empty() { - return Ok(trimmed.to_string()); - } - } - - if let Some(value) = get_config_value(SNAPSHOT_MANIFEST_CONFIG_KEY)? { - let trimmed = value.trim(); - if !trimmed.is_empty() { - return Ok(trimmed.to_string()); - } - } - - Err(IdxError::InvalidInput(format!( - "ownership sync needs a snapshot manifest; pass `--manifest` or set `{SNAPSHOT_MANIFEST_CONFIG_KEY}` / `{SNAPSHOT_MANIFEST_ENV}`" - ))) + Ok(DEFAULT_SNAPSHOT_MANIFEST_URL.to_string()) } pub fn fetch_manifest(source: &str) -> Result { @@ -687,8 +685,9 @@ mod tests { use rusqlite::Connection; use super::{ - OwnershipSnapshotArtifact, OwnershipSnapshotManifest, OwnershipSnapshotSource, - OwnershipSyncAction, SNAPSHOT_MANIFEST_SCHEMA_VERSION, build_sync_decision, parse_manifest, + DEFAULT_SNAPSHOT_MANIFEST_URL, OwnershipSnapshotArtifact, OwnershipSnapshotManifest, + OwnershipSnapshotSource, OwnershipSyncAction, SNAPSHOT_MANIFEST_SCHEMA_VERSION, + build_sync_decision, parse_manifest, resolve_manifest_source_with, }; use crate::ownership::db::{ensure_schema, insert_release}; use crate::ownership::types::OwnershipRelease; @@ -815,6 +814,36 @@ mod tests { assert!(parsed.source.is_none()); } + #[test] + fn resolve_manifest_source_uses_built_in_default_when_unset() { + let resolved = resolve_manifest_source_with(None, None, None).expect("default manifest"); + assert_eq!(resolved, DEFAULT_SNAPSHOT_MANIFEST_URL); + } + + #[test] + fn resolve_manifest_source_prefers_explicit_then_env_then_config() { + let explicit = resolve_manifest_source_with( + Some("https://example.com/explicit.json"), + Some("https://example.com/env.json"), + Some("https://example.com/config.json"), + ) + .expect("explicit manifest"); + assert_eq!(explicit, "https://example.com/explicit.json"); + + let env = resolve_manifest_source_with( + Some(" "), + Some("https://example.com/env.json"), + Some("https://example.com/config.json"), + ) + .expect("env manifest"); + assert_eq!(env, "https://example.com/env.json"); + + let config = + resolve_manifest_source_with(None, Some(" "), Some("https://example.com/config.json")) + .expect("config manifest"); + assert_eq!(config, "https://example.com/config.json"); + } + #[test] fn ensure_schema_can_store_release_metadata_needed_for_snapshots() { let conn = Connection::open_in_memory().unwrap();