feat(audit): fit the audit tools into the agent surface (P8)
After P5 merged first: a close leads to the audit and the period's attestation, an audit run to its workpaper and the claims behind it, the attest tools to the attestations and the CPA package; a refusal for want of a standing audit is the guard audit_required with books.audit as its remedy; the audit prompt runs the audit tools. policy.cpa_package_requires_audit, unset, refuses once the books hold an audit run and warns before, so a book that has not begun the programme is not stopped from its first handoff by a rule it has not met; set to refuse, it holds from the start. bookspec states B-45 (convention) in its checker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
45bcce679b
commit
4774bd9abd
14 files changed
+139
-33
No files matched your search
@@ -40,11 +40,14 @@ pub(crate) enum Guard {
|
||||
DuplicateVendorRef,
|
||||
/// The first invoice number cannot change once one is issued.
|
||||
FirstNumberLocked,
|
||||
/// No audit stands at the books' current head to rest this on (plan
|
||||
/// P8): run `books.audit`.
|
||||
AuditRequired,
|
||||
}
|
||||
|
||||
impl Guard {
|
||||
/// Every guard, for the registry test.
|
||||
pub(crate) const ALL: [Guard; 10] = [
|
||||
pub(crate) const ALL: [Guard; 11] = [
|
||||
Guard::StalePreview,
|
||||
Guard::FormatUpgradePending,
|
||||
Guard::ReceiptRequired,
|
||||
@@ -55,6 +58,7 @@ impl Guard {
|
||||
Guard::UntaggedControlLine,
|
||||
Guard::DuplicateVendorRef,
|
||||
Guard::FirstNumberLocked,
|
||||
Guard::AuditRequired,
|
||||
];
|
||||
|
||||
/// The code an agent reads.
|
||||
@@ -70,6 +74,7 @@ impl Guard {
|
||||
Guard::UntaggedControlLine => "untagged_control_line",
|
||||
Guard::DuplicateVendorRef => "duplicate_vendor_ref",
|
||||
Guard::FirstNumberLocked => "first_number_locked",
|
||||
Guard::AuditRequired => "audit_required",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +96,7 @@ impl Guard {
|
||||
Guard::UntaggedControlLine => "books.control_check",
|
||||
Guard::DuplicateVendorRef => "books.search_entries",
|
||||
Guard::FirstNumberLocked => "books.settings",
|
||||
Guard::AuditRequired => "books.audit",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -119,8 +119,11 @@ pub(crate) fn tool_steps(
|
||||
let with_entry = |id: u64| Some(json!({ "entry_id": id }));
|
||||
// A preview: what is missing first, then the human confirms, then the
|
||||
// same call with the digest.
|
||||
// An audit already run is not previewed: its run is the answer.
|
||||
let already_run = tool == "books.audit"
|
||||
&& result.get("status").and_then(Value::as_str) == Some("already_run");
|
||||
if let (false, Some(preview), Some(args)) = (
|
||||
confirmed,
|
||||
confirmed || already_run,
|
||||
result.get("preview").and_then(Value::as_str),
|
||||
args,
|
||||
) {
|
||||
@@ -301,12 +304,79 @@ pub(crate) fn tool_steps(
|
||||
}
|
||||
}
|
||||
}
|
||||
"books.close_period" => steps.push(Step::tool(
|
||||
"books.verify",
|
||||
None,
|
||||
"Verify the books after a close: the chain, the replay and every stored file.",
|
||||
"books.close_period" => {
|
||||
let period = result
|
||||
.get("period_id")
|
||||
.or_else(|| args.and_then(|a| a.get("period_id")))
|
||||
.and_then(Value::as_u64);
|
||||
steps.push(Step::tool(
|
||||
"books.audit",
|
||||
Some(json!({ "scope": "month" })),
|
||||
"Audit the books through the month's end; the attestation rests on that run.",
|
||||
false,
|
||||
));
|
||||
steps.push(Step::tool(
|
||||
"books.attest_period",
|
||||
period.map(|p| json!({ "period": p.to_string() })),
|
||||
"Once the audit is run and the receipts read, attest the period you closed.",
|
||||
false,
|
||||
));
|
||||
}
|
||||
"books.audit" => {
|
||||
if let Some(id) = result.pointer("/run/id").and_then(Value::as_u64) {
|
||||
steps.push(Step::tool(
|
||||
"books.workpaper",
|
||||
Some(json!({ "id": id })),
|
||||
"Read the workpaper: what was checked and every finding. Fix findings with journaled corrections, then audit again.",
|
||||
false,
|
||||
));
|
||||
steps.push(Step::tool(
|
||||
"books.attest_receipt",
|
||||
None,
|
||||
"Have separate verifier agents read each receipt (books.get_receipt, then its page image) and attest it.",
|
||||
false,
|
||||
));
|
||||
steps.push(Step::tool(
|
||||
"books.review_entry",
|
||||
None,
|
||||
"Review each posted entry whole.",
|
||||
false,
|
||||
));
|
||||
steps.push(Step::tool(
|
||||
"books.attest_period",
|
||||
None,
|
||||
"Then attest the period this audit covers.",
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
"books.attest_receipt" => steps.push(Step::tool(
|
||||
"books.review_entry",
|
||||
args.and_then(|a| a.get("entry_id"))
|
||||
.map(|e| json!({ "entry_id": e })),
|
||||
"Review the whole entry the receipt supports.",
|
||||
false,
|
||||
)),
|
||||
"books.review_entry" => steps.push(Step::tool(
|
||||
"books.attestations",
|
||||
None,
|
||||
"See which claims count at the bar.",
|
||||
false,
|
||||
)),
|
||||
"books.attest_period" => {
|
||||
steps.push(Step::tool(
|
||||
"books.attestations",
|
||||
None,
|
||||
"See which claims count at the bar before the CPA package.",
|
||||
false,
|
||||
));
|
||||
steps.push(Step::tool(
|
||||
"books.export_cpa_package",
|
||||
None,
|
||||
"When the year is audited and attested at the bar, hand the books to the CPA.",
|
||||
false,
|
||||
));
|
||||
}
|
||||
"books.export_cpa_package" => {
|
||||
if let Some(cmd) = result.pointer("/file/download").and_then(Value::as_str) {
|
||||
steps.push(Step {
|
||||
|
||||
@@ -683,7 +683,15 @@ impl BooksServer {
|
||||
.find(|q| q.name == named || q.id.to_string() == named)
|
||||
.map(|q| q.id)
|
||||
.ok_or_else(|| format!("no period {named:?}: books.list_periods lists them"))?;
|
||||
let (a, _) = store.attest_period(period, claim).map_err(err)?;
|
||||
let (a, _) = store.attest_period(period, claim).map_err(|e| match e {
|
||||
ledger_store::StoreError::AuditRefused(why) => serde_json::json!({
|
||||
"code": "audit_required",
|
||||
"message": why,
|
||||
"remedy_tool": "books.audit",
|
||||
})
|
||||
.to_string(),
|
||||
other => other.to_string(),
|
||||
})?;
|
||||
Ok(Json(attest_out(&store, &a)?))
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ pub(crate) const PROMPTS: [PromptDef; 6] = [
|
||||
PromptDef {
|
||||
name: "audit",
|
||||
title: "Audit the books",
|
||||
description: "The adversarial procedure over a scope: verify, control checks, evidence against entries.",
|
||||
description: "The adversarial procedure over a scope: the audit run, verify, control checks, evidence against entries, attestations.",
|
||||
args: &[(
|
||||
"scope",
|
||||
"month, annual, cpa_handoff or full.",
|
||||
|
||||
@@ -1155,7 +1155,11 @@ impl BooksServer {
|
||||
// were: there is no audit to ask for.
|
||||
let mut audit_warning: Option<String> = None;
|
||||
if let Some(why) = self.cpa_audit_standing(&store)? {
|
||||
if store.ledger().settings().cpa_package_requires_audit() {
|
||||
let refuse = match store.ledger().settings().cpa_package_requires_audit() {
|
||||
Some(explicit) => explicit,
|
||||
None => !store.audit_runs().map_err(|e| e.to_string())?.is_empty(),
|
||||
};
|
||||
if refuse {
|
||||
let Some(reason) = p
|
||||
.without_audit_reason
|
||||
.as_deref()
|
||||
|
||||
@@ -5248,7 +5248,7 @@ pub struct ReconciliationsOut {
|
||||
/// Parameters for `books.handbook`.
|
||||
#[derive(Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct HandbookParams {
|
||||
/// The section to read: contract, entries, drafts-and-reversals, evidence-and-receipts, attachments, files-in-out, reconciliation, fixed-assets, prepaid-and-recurring, revenue-and-payouts, projects, parties-and-tins, 1099, cpa-handoff, settings-and-conventions, viewer, backups, self-host, format-upgrade, rules-that-change, control-roles, tracing-figures, invoices-and-bills, or "all" for the whole handbook in order. Omit for the index.
|
||||
/// The section to read: contract, entries, drafts-and-reversals, evidence-and-receipts, attachments, files-in-out, reconciliation, fixed-assets, prepaid-and-recurring, revenue-and-payouts, projects, parties-and-tins, 1099, cpa-handoff, settings-and-conventions, viewer, backups, self-host, format-upgrade, rules-that-change, control-roles, tracing-figures, invoices-and-bills, attestations-and-audit, or "all" for the whole handbook in order. Omit for the index.
|
||||
#[serde(default)]
|
||||
pub section: Option<String>,
|
||||
}
|
||||
@@ -5896,7 +5896,7 @@ pub struct AuditParams {
|
||||
pub through: Option<String>,
|
||||
/// month (a month-end attestation), annual (a fiscal year),
|
||||
/// cpa_handoff (the year, for the CPA package) or full (everything
|
||||
/// since the first entry).
|
||||
/// from the start of the books).
|
||||
pub scope: String,
|
||||
/// True: run the audit and file its workpaper. Without it, a preview
|
||||
/// that lists what will be checked and changes nothing.
|
||||
|
||||
Reference in new issue
Block a user