feat(mcp): page get_ownership_changes and summarize all changes

Between the two latest releases there are 1,568 holder changes. With only
`limit` (up to 2000, ~200 KB), agents that wanted them all overflowed
their tool output: Hermes then paged ticker by ticker (32 calls), and
opencode 1 and 2 did the same and timed out.

- `offset` pages through the rows; responses carry `offset` and
  `next_offset` (null after the last page). Pages cap at 300 rows
  (~40 KB); the over-limit error points to `offset` and `summary`.
- Rows come largest |delta| first, so the first page holds the moves
  that matter rather than tickers starting with A.
- `summary` covers every row: counts by change type, the number of
  tickers, and the ten tickers with the most changes.

The CLI's `ownership changes` is unchanged. tools.json and the website's
MCP pages are regenerated for the new description and parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
hermes 2026-10-16 08:13:30 +00:00
commit db7c90861c
6 changed files with 124 additions and 12 deletions

View file

@ -346,6 +346,44 @@ fn ownership_tools_read_the_local_database() {
);
assert_eq!(limited["total"], total);
assert_eq!(limited["changes"].as_array().unwrap().len(), 1);
assert_eq!(limited["next_offset"], 1);
// Pages cover every row exactly once, largest change first.
let mut paged = Vec::new();
let mut offset = Some(0);
while let Some(next) = offset {
let page = client.ok(
"get_ownership_changes",
json!({"from": "2026-01-30", "to": "2026-02-27", "limit": 2, "offset": next}),
);
paged.extend(page["changes"].as_array().unwrap().iter().cloned());
offset = page["next_offset"].as_u64();
}
assert_eq!(paged.len() as u64, total);
assert_eq!(&paged, all["changes"].as_array().unwrap());
let deltas: Vec<_> = paged
.iter()
.map(|row| row["delta_bps"].as_i64().unwrap().abs())
.collect();
assert!(
deltas.windows(2).all(|pair| pair[0] >= pair[1]),
"{deltas:?}"
);
let by_type = all["summary"]["by_type"].as_object().unwrap();
assert_eq!(
by_type.values().map(|n| n.as_u64().unwrap()).sum::<u64>(),
total
);
assert!(all["summary"]["tickers"].as_u64().unwrap() >= 1, "{all}");
let (is_error, body) = client.call(
"get_ownership_changes",
json!({"from": "2026-01-30", "to": "2026-02-27", "limit": 301}),
);
assert!(is_error, "{body}");
assert_eq!(body["code"], "INVALIDINPUT");
assert!(body.to_string().contains("offset"), "{body}");
let one = client.ok(
"get_ownership_changes",