mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(ownership): correct changes, alias search, graph, and name lookups
From a full-repo Codex audit, each reproduced on the synced ownership DB:
- `changes` paired an investor's accounts across releases instead of
comparing totals: KREN's KRESNA PRIMA INVEST went 9.44% -> 13.45% (a
second 4.01% account) but was reported as decreased 944 -> 401 bps.
Sum a holder's accounts per ticker, and read one release per date (the
latest imported, as other queries do), so a corrected file supersedes
the original instead of mixing with it.
- `changes` / get_ownership_changes accepted any dates: `from: ""`
reported all 7,154 holders as new, unknown dates returned no changes.
Dates must be YYYY-MM-DD imported releases with `from` before `to`.
- get_investor_holdings / `entity` with an empty name matched every
investor and returned an arbitrary one ("AMY"); `%` and `_` acted as
LIKE wildcards. Reject empty names; match wildcards literally.
- Alias search never worked: the FTS table declared an `aliases` column
its content table lacks (`no such column: T.aliases`), so searches
fell back to canonical names and a raw KSEI name such as
"PT ASTRA INTERNATIONAL TBK" found nothing. Search names and aliases
directly, ranked exact name, exact alias, prefix, then shorter names.
- `graph` enumerated every path (exponential in depth) and its cycle
check matched substrings, dropping entity:1 when entity:10 was on the
path. Traverse breadth-first with an exact visited set.
- Name normalization removed leading PT tokens from the front of a Vec,
quadratic in hostile names; narrow a slice instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
f5da3b745e
commit
1d3fe5ca88
6 changed files with 437 additions and 196 deletions
|
|
@ -109,34 +109,25 @@ pub fn normalize_name(raw: &str) -> String {
|
|||
return upper;
|
||||
}
|
||||
|
||||
let mut tokens: Vec<&str> = upper.split_whitespace().collect();
|
||||
|
||||
loop {
|
||||
let mut changed = false;
|
||||
|
||||
if let Some(first) = tokens.first().copied()
|
||||
&& matches!(first, "PT" | "PT.")
|
||||
{
|
||||
tokens.remove(0);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if let Some(last) = tokens.last().copied()
|
||||
&& matches!(
|
||||
last,
|
||||
"TBK" | "Tbk" | "(PERSERO)" | "LIMITED" | "PTE" | "LTD"
|
||||
)
|
||||
{
|
||||
let _ = tokens.pop();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
let tokens: Vec<&str> = upper.split_whitespace().collect();
|
||||
// Trim prefix and suffix tokens by narrowing a slice: linear, where
|
||||
// removing from the front of the Vec was quadratic in hostile names.
|
||||
let mut kept = tokens.as_slice();
|
||||
while let [first, rest @ ..] = kept
|
||||
&& matches!(*first, "PT" | "PT.")
|
||||
{
|
||||
kept = rest;
|
||||
}
|
||||
while let [rest @ .., last] = kept
|
||||
&& matches!(
|
||||
*last,
|
||||
"TBK" | "Tbk" | "(PERSERO)" | "LIMITED" | "PTE" | "LTD"
|
||||
)
|
||||
{
|
||||
kept = rest;
|
||||
}
|
||||
|
||||
collapse_whitespace(&tokens.join(" "))
|
||||
collapse_whitespace(&kept.join(" "))
|
||||
}
|
||||
|
||||
/// Convert a KseiRawRow into a normalized KseiHoldingDraft.
|
||||
|
|
@ -357,6 +348,14 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_name_strips_many_prefixes_in_linear_time() {
|
||||
// Removing from the front of a Vec made this quadratic (~5e9 shifts).
|
||||
let hostile = format!("{}ACME TBK", "PT ".repeat(100_000));
|
||||
assert_eq!(normalize_name(&hostile), "ACME");
|
||||
assert_eq!(normalize_name("PT TBK"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_name() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue