mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 01:35:36 +00:00
- Remove FEATURE_SPEC.md, TODO.md, and docs/AGENT_INTEGRATION.md; their core gaps have shipped. Open work moves to a short ROADMAP.md. - Remove the CLAUDE.md symlink and rewrite AGENTS.md as a quick guide: navigation, rules, and the nix flake check gate. - Merge OWNERSHIP_SELF_HOSTED.md into OWNERSHIP_PUBLISH.md and drop private host integration notes. - ARCHITECTURE: add MSN endpoints, clarify Bing ingest is unsupported. - OWNERSHIP_SYNC: note source.pdf_url also carries XLSX URLs. - systemd example: --history 2 so snapshots stay under the 10 MiB client cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.2 KiB
5.2 KiB
Conventions
Design Philosophy
Schema-Driven Development
Define data types FIRST, then build logic around them. Types are the spec.
// ✅ Good: rich type with documented fields, integer precision
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KseiHolding {
/// Ownership percentage in basis points: 41.10% → 4110.
pub percentage_bps: i64,
/// Total shares held (absolute count).
pub total_shares: i64,
}
// ❌ Bad: stringly-typed, float precision
pub struct Holding {
pub percentage: f64, // what unit? what precision?
pub shares: String, // why string?
}
Functional Approach
Parse/transform functions are pure: take input, return Result<T, IdxError>, no side effects.
// ✅ Good: pure function, testable in isolation
pub fn parse_id_number(s: &str) -> Result<i64, IdxError> { ... }
pub fn normalize_name(raw: &str) -> String { ... }
pub fn parse_quote_from_str(symbol: &str, raw: &str) -> Result<Quote, IdxError> { ... }
// ❌ Bad: function with side effects, hard to test
pub fn fetch_and_save_quote(symbol: &str) -> Result<(), IdxError> { ... }
Types Over Primitives
Use newtypes, enums, and rich structs. Avoid String where a domain type exists.
// ✅ Good
pub struct InvestorTypeCode(pub String);
pub enum Locality { Local, Foreign }
pub enum FlowSignal { Holder, Buyer, Seller, NewPosition, Exited }
// ❌ Bad
pub type InvestorType = String;
pub type Locality = String;
Naming
Files
types.rs— domain data types for a modulemod.rs— module declarations and re-exportsclient.rs— HTTP client codemap.rs/parse.rs— response mapping / parsing functionsraw_types.rs— raw API response shapes (before normalization)
Functions
parse_*— deserialize raw data into domain typesnormalize_*— clean/transform data (names, numbers, dates)resolve_*— lookup/match entitiesquery_*— read from databasefetch_*— HTTP requests to external APIsrender_*— output formatting (tables, JSON)handle— CLI command dispatch entry point
Types
*Raw/*RawRow— pre-normalization data (strings from API/PDF)*Holding— ownership fact row*Metrics— computed analytics*Row— display-ready composite type*Args— clap command arguments
Patterns
Provider Trait Pattern
pub trait QuoteProvider {
fn quote(&self, symbol: &str) -> Result<Quote, IdxError>;
}
// Factory function, not direct construction
pub fn default_provider(kind: ProviderKind) -> Box<dyn MarketDataProvider> { ... }
Parse Pipeline Pattern
// Raw API response → domain type, always via parse function
let raw: &str = &response_body;
let quote = yahoo::parse_quote_from_str("BBCA.JK", raw)?;
DB Function Pattern
// Take &Connection, caller manages lifetime. Use transactions for bulk.
pub fn insert_ksei_holdings(conn: &Connection, holdings: &[KseiHolding]) -> Result<usize, IdxError> { ... }
pub fn query_ticker_holdings(conn: &Connection, code: &str) -> Result<TickerOwnership, IdxError> { ... }
Error Propagation
// Map external errors to IdxError variants
let conn = Connection::open(path)
.map_err(|e| IdxError::DatabaseError(e.to_string()))?;
Integer Precision
- Percentages: basis points (i64).
41.10%→4110 - Shares: absolute count (i64). No floats.
- Prices: whole IDR (i64). Rounded from float at parse boundary.
- Money (USD): whole dollars (i64) for Bing data.
Testing
- Canonical verification:
nix flake check -Llocally and before pushing; CI uses the same gate. Do not repeat full Cargo build/lint/test runs after a successful flake check. - Focused debugging: use
nix develop --command cargo test <filter>when iterating on one failure. - Unit tests: retain precise parser, normalization, calculation, and boundary regressions; avoid pinning incidental wording, enum counts, or internal wiring.
- Integration tests: prefer CLI workflows, real temporary/in-memory SQLite, and deterministic local provider fixtures for user-visible contracts and state transitions.
- Output contracts: parse JSON from stdout on success and stderr on failure; errors must leave stdout empty. Assert meaningful values, types, and stable error codes rather than substrings or diagnostic wording.
- Stateful workflows: compare cached data across offline/failure transitions, and verify failed snapshot updates preserve both database bytes and query results. Keep packaged-binary smoke representative; put exhaustive boundaries in integration tests.
- Fixtures:
tests/fixtures/*.json— real API responses, sanitized - No live API calls in CI —
IDX_USE_MOCK_PROVIDER=1 - Test naming:
test_<function>_<scenario>(e.g.,test_parse_id_number_with_dots) - Coverage over counts: replacing redundant unit assertions with stronger integration scenarios must retain meaningful error, boundary, and data-integrity coverage.
Git / VCS
- Push via
nix develop --command git push(for prek hooks) - Branch naming:
feat/<name>,fix/<name>,docs/<name> - Commit messages: conventional commits (
feat:,fix:,refactor:,docs:)