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
@@ -290,6 +290,14 @@ fn a_period_is_attested_on_a_standing_audit() {
|
||||
#[test]
|
||||
fn cpa_package_refuses_without_a_counted_audit_unless_a_reason_is_given() {
|
||||
let (_books, mut w, paper, rid, bare) = setup("p8-cpa");
|
||||
// Unset, the package warns until the books hold an audit run; the
|
||||
// owner who wants it refused from the start says so.
|
||||
answer_of(&w.call(
|
||||
"books.set_setting",
|
||||
json!({"decided_by": "Avery Quill",
|
||||
"changes": [{"key": "policy.cpa_package_requires_audit", "value": "refuse"}]}),
|
||||
))
|
||||
.unwrap();
|
||||
let export = |w: &mut common::Wire, extra: Value| {
|
||||
let mut args = json!({"as_of": "2026-03-31", "cash_accounts": ["1000"],
|
||||
"include_receipts": false, "include_tins": false});
|
||||
|
||||
@@ -418,7 +418,7 @@ pub static KEYS: &[Key] = &[
|
||||
toml: ("books", "cpa_package_requires_audit"),
|
||||
kind: Kind::Choice(AUDIT_REQUIRED),
|
||||
sensitivity: Sensitivity::Public,
|
||||
doc: "Whether books.export_cpa_package waits for an audit: refuse (the package is refused unless an audit for the handoff or a full audit at the current books has its period attested; a reason given for the export overrides it and is recorded as a decision) or warn (the package is made and says so). Unset: refuse.",
|
||||
doc: "Whether books.export_cpa_package waits for an audit: refuse (the package is refused unless an audit for the handoff or a full audit at the current books has its period attested; a reason given for the export overrides it and is recorded as a decision) or warn (the package is made and says so). Unset: refuse once the books hold an audit run, warn before.",
|
||||
},
|
||||
Key {
|
||||
name: "backup.git_remote",
|
||||
@@ -1639,9 +1639,16 @@ impl Settings {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `policy.cpa_package_requires_audit` (unset: refuse).
|
||||
pub fn cpa_package_requires_audit(&self) -> bool {
|
||||
self.str("policy.cpa_package_requires_audit") != Some("warn")
|
||||
/// `policy.cpa_package_requires_audit`: `Some(true)` for refuse,
|
||||
/// `Some(false)` for warn, `None` when unset (the package then refuses
|
||||
/// once the books hold an audit run, and warns before: the owner has
|
||||
/// not begun the programme).
|
||||
pub fn cpa_package_requires_audit(&self) -> Option<bool> {
|
||||
match self.str("policy.cpa_package_requires_audit") {
|
||||
Some("warn") => Some(false),
|
||||
Some(_) => Some(true),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `backup.git_remote`, if set.
|
||||
|
||||
@@ -7,7 +7,7 @@ The receipt rule in force: {evidence_rule}
|
||||
## Running an audit
|
||||
|
||||
- books.audit { through, scope } without confirmed lists the checks and changes nothing; show it to the human. Then call it again with confirmed: true and the preview. It files the workpaper (JSON and PDF, category workpaper) and journals the run. books.workpaper { id } gives it back: the JSON, and the PDF handed over (run its download command to save it).
|
||||
- scope: month (a month-end), annual (a fiscal year), cpa_handoff (the year, for the CPA package), full (everything since the first entry).
|
||||
- scope: month (a month-end), annual (a fiscal year), cpa_handoff (the year, for the CPA package), full (everything from the start of the books).
|
||||
- It checks the trial balance, the accounting equation, the profit and loss against equity, the cash flow's footing, the A/R and A/P control accounts, closes, schedules and depreciation, the asset register, every posted entry against the receipt rule, open drafts, tax categories, 1099 decisions and the settings; then the journal's chain, the stored files, the access log and the backups. A finding is a failure (the books are wrong or cannot be shown right), a warning (unfinished or a rule not met) or a note (listed so the workpaper is whole: a reversal rests on its original's receipt).
|
||||
- Fix findings with journaled corrections (a reversal, a receipt attached, a decision recorded), never by editing: posted entries stay as they are. Then audit again.
|
||||
- The same books, day and scope give the run that exists: asking again files nothing. The books are "the same" while no entry, document, close, settings or role change has been made; filing a receipt, an audit or an attestation does not change them.
|
||||
@@ -18,7 +18,7 @@ The receipt rule in force: {evidence_rule}
|
||||
Use separate verifier agents; none of them should be the one that made the entry. For each receipt:
|
||||
|
||||
1. books.get_receipt for the extracted text, then its page image, and read the total and the date off the receipt itself.
|
||||
2. books.attest_receipt { attachment_id, entry_id, amount, date, method, note }. The books compare what you read with the entry's amounts and date. A difference is recorded as disagrees and never counts: do not adjust your reading to fit.
|
||||
2. books.attest_receipt { attachment_id, entry_id, amount, date, method, note }. The books compare what you read with the entry's amounts and date. A difference is recorded as disagrees and never counts: never bend your reading to fit.
|
||||
3. books.review_entry { entry_id, method } for each posted entry, once its lines, accounts, party and evidence have been read whole.
|
||||
4. books.attest_period { period, method } last, on a stand-still: it is refused unless an audit through the period's end still stands, which it does only while nothing has been posted, created, closed or set since the audit. If it is refused, audit again.
|
||||
|
||||
@@ -30,4 +30,4 @@ books.attestations lists every claim with counted: whether it meets policy.attes
|
||||
|
||||
## The CPA package
|
||||
|
||||
Under policy.cpa_package_requires_audit = refuse (the default once audits are declared), books.export_cpa_package is refused unless an audit for the handoff (scope cpa_handoff or full) stands at the current books with a period attested on it at the bar. Pass without_audit_reason only when the human says to hand it over anyway; the reason is journaled as a decision. Under warn the package is made and says so.
|
||||
Under policy.cpa_package_requires_audit = refuse (the default once the books hold an audit run; before one, the package is made and warns), books.export_cpa_package is refused unless an audit for the handoff (scope cpa_handoff or full) stands at the current books with a period attested on it at the bar. Pass without_audit_reason only when the human says to hand it over anyway; the reason is journaled as a decision. Under warn the package is made and says so.
|
||||
@@ -1,7 +1,8 @@
|
||||
Audit the books for scope {{scope}} (month, annual, cpa_handoff or full). Treat the books as something to disprove: look for what is wrong.
|
||||
|
||||
1. books.verify: the chain, the replay, the database and every stored file.
|
||||
2. books.control_check: the receivable and payable controls agree with their documents.
|
||||
3. books.digest for the scope's period, and read every posted entry in it against its evidence: books.search_entries, books.get_entry, books.get_receipt. An entry that needs a receipt and has none, or whose receipt shows another amount, date or party, is a finding.
|
||||
4. books.trial_balance, books.profit_and_loss and books.balance_sheet for the period; compare them with the previous period and explain each large movement.
|
||||
5. Report findings to the human as a list: what, where (entry or account), why it matters, and the tool that fixes it. Fix nothing without the human's confirmation; corrections are reversing entries, not edits.
|
||||
1. books.audit with scope {{scope}} and the day the audit covers: preview it, show the human what it will check, then run it with the preview. It checks the statements, the control accounts, closes, schedules, assets, every posted entry against the receipt policy, drafts, tax categories, 1099 decisions, settings, the chain, the stored files, the access log and the backups. books.workpaper gives the findings back.
|
||||
2. books.verify and books.control_check: the chain, the replay and every stored file, and the receivable and payable controls against their documents.
|
||||
3. books.digest for the scope's period, and read every posted entry in it against its evidence: books.search_entries, books.get_entry, books.get_receipt (its text first, then the page image). An entry that needs a receipt and has none, or whose receipt shows another amount, date or party, is a finding.
|
||||
4. Use separate verifier agents, none of them the one that made the entry. For each receipt, books.attest_receipt with the amount and date as read off it; for each entry, books.review_entry; say truthfully how each checked (method). A reading that differs from the entry never counts: never bend it to fit.
|
||||
5. books.trial_balance, books.profit_and_loss and books.balance_sheet for the period; compare them with the previous period and explain each large movement.
|
||||
6. Report findings to the human as a list: what, where (entry or account), why it matters, and the tool that fixes it. Fix nothing without the human's confirmation; corrections are reversing entries, not edits. Then audit again, and when it is clean books.attest_period; books.attestations shows which claims count at the bar.
|
||||
@@ -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