mirror of
https://github.com/we-promise/sure.git
synced 2026-08-04 16:12:14 +00:00
Three findings from the automated review passes, all confirmed against the code before changing anything. The download URL was dead on arrival for the caller it was built for. Sure serves stored files through Active Storage controllers that config/initializers/active_storage_authorization.rb gates on `viewable_by?(Current.user)` — a signed-in browser session. An MCP client has a bearer token and no session, so following the URL would have redirected to sign-in. Removed it rather than leaving a link that cannot work, and the description now points at search_family_files or the vault UI. Coverage called a month `covered` when a document merely existed. An unreconciled statement is not mismatched, so it took the `covered` branch, and the payload carried nothing to correct the reading — the same "advertised verification that never happened" bug fixed last round in get_account_statement, in a second place. Months now carry their own reconciliation_status, and the description says covered means presence, not agreement. Listing filtered visibility after limiting. Beyond underfilling a page, with no cursor and a 100-row cap an accessible statement behind enough newer invisible ones was unreachable. Visibility now lives in the query, mirroring viewable_by? for a statement manager. Also: rescue unexpected upload failures into a tool error instead of a raw exception string, derive the documented size limit from MAX_FILE_SIZE, list every coverage status in mcp.md, and cover the failed-reconciliation and base64-normalisation branches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn
115 lines
4.0 KiB
Ruby
115 lines
4.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Assistant::Function::GetAccountStatement < Assistant::Function
|
|
include Assistant::Function::StatementVaultSupport
|
|
|
|
class << self
|
|
def name
|
|
"get_account_statement"
|
|
end
|
|
|
|
def description
|
|
<<~INSTRUCTIONS
|
|
Fetch one Statement Vault document by ID: its identity (SHA-256, filename,
|
|
period), any balances recorded for it, and reconciliation checks against
|
|
the ledger.
|
|
|
|
IMPORTANT — reconciliation is usually empty, and its absence means nothing.
|
|
The checks only exist once a human has typed the statement's opening and
|
|
closing balances into the Statement Vault UI. Nothing extracts them from
|
|
the document, so a statement you uploaded through `upload_account_statement`
|
|
comes back with `reconciliation_checks: []` and
|
|
`reconciliation_status: "unavailable"`. That is "nobody has entered the
|
|
figures", NOT "the document agrees with the ledger". Never report an
|
|
unreconciled statement as verified.
|
|
|
|
When the balances have been entered, each check compares that figure
|
|
against the account's recorded balance:
|
|
|
|
- `opening_balance` / `closing_balance` — the statement's figure vs the ledger's
|
|
- `period_movement` — the change across the period, both sides
|
|
|
|
Each reports `matched` or `mismatched` (tolerance: 0.01). A mismatch means
|
|
the ledger and the document disagree — report it, and do not paper over it
|
|
by adjusting the number to fit. Note this is ledger agreement only: nothing
|
|
here verifies that the document's own line items sum to its printed total.
|
|
That parse-integrity check belongs to whatever extracted the figures.
|
|
|
|
This does NOT return the document's bytes and cannot give you a link that
|
|
works for you: Sure serves stored files only to a signed-in browser
|
|
session, which an MCP client does not have. To read a document, either
|
|
search its contents with `search_family_files`, or point the user at
|
|
Settings -> Statement Vault to open it themselves.
|
|
|
|
Example:
|
|
|
|
```
|
|
get_account_statement({ statement_id: "abc123-def456" })
|
|
```
|
|
INSTRUCTIONS
|
|
end
|
|
end
|
|
|
|
def strict_mode?
|
|
false
|
|
end
|
|
|
|
def params_schema
|
|
build_schema(
|
|
required: [ "statement_id" ],
|
|
properties: {
|
|
statement_id: {
|
|
type: "string",
|
|
description: "UUID of the statement, as returned by list_account_statements or upload_account_statement."
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
def call(params = {})
|
|
return not_a_statement_manager unless statement_manager?
|
|
|
|
statement = find_statement(params["statement_id"])
|
|
|
|
unless statement&.viewable_by?(user)
|
|
return error("not_found", "No statement found with that ID that this user can view.")
|
|
end
|
|
|
|
checks = reconciliation_payload(statement)
|
|
status = statement.reconciliation_status
|
|
|
|
{
|
|
success: true,
|
|
statement: statement_payload(statement).merge(
|
|
reconciliation_status: status,
|
|
reconciliation_checks: checks,
|
|
# Spelled out in the payload, not just the tool description: an agent
|
|
# reading only the JSON must not read an empty check list as agreement.
|
|
reconciliation_note: unavailable_note(status)
|
|
).compact
|
|
}
|
|
end
|
|
|
|
private
|
|
def unavailable_note(status)
|
|
return nil unless status == "unavailable"
|
|
|
|
"No reconciliation has been performed: this statement has no opening/closing " \
|
|
"balances recorded, and nothing extracts them from the document. Someone must " \
|
|
"enter them in the Statement Vault UI. This is not evidence that the statement " \
|
|
"agrees with the ledger."
|
|
end
|
|
|
|
def reconciliation_payload(statement)
|
|
statement.reconciliation_checks.map do |check|
|
|
{
|
|
check: check[:key],
|
|
statement_amount: check[:statement_amount].to_s,
|
|
ledger_amount: check[:ledger_amount].to_s,
|
|
difference: check[:difference].to_s,
|
|
status: check[:status]
|
|
}
|
|
end
|
|
end
|
|
end
|