feat(core,mcp,gui): never apply a legal rule past what wanderbooks knows
Stephen (2026-10-13): when what wanderbooks knows is stale, check the date, research it or ask the owner or CPA, and update the rule for everyone with a pull request (best) or an issue (worst). - ledger_core::rules: a registry of rules set by law that change over time. Each says the years it's known for, its rules.md entry, its vendored official source, and a review date. stale_rules(today) is pure; the engine has no clock. - The 1099 threshold is the first: $600 through 2025, $2,000 in 2026, then indexed for inflation (26 U.S.C. §6041(h)). default_threshold and vendor_1099_report now return Result; for 2027+ they refuse with LedgerError::RuleNotKnown, whose message says what to do: get the figure from the official source or the owner/CPA, pass it explicitly, then open a PR (or issue) at github.com/swaits/wanderbooks. Before, 2027+ silently used $2,000. - MCP: books.vendor_1099 relays that refusal; books.digest gains stale_rules; the server instructions tell the agent the procedure (and how to handle control-account conflicts: explain, never renumber). The CPA package writes a NOT-INCLUDED note instead of a guessed 1099 worksheet. - Viewer: "Needs attention" shows each stale rule with a ready prompt for the agent; the 1099 page explains instead of guessing. Review date for the 1099 rule: 2026-11-01, when the IRS usually publishes next year's inflation adjustments (CONVENTION, marked as such in the registry). Tests: an_unknown_year_threshold_is_never_guessed, dated_rules_go_stale_on_their_review_date (dates passed in, so no clock in the test), MCP stdio: 2027 refused with guidance, explicit threshold accepted, digest carries stale_rules (asserted date-independently). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
e2bebce051
commit
df38dcccf9
15 files changed
+349
-47
No files matched your search
@@ -86,7 +86,7 @@ fn books() -> Ledger {
|
||||
#[test]
|
||||
fn cash_paid_per_vendor_and_who_is_reportable() {
|
||||
let l = books();
|
||||
let r = vendor_1099_report(&l, 2026, None);
|
||||
let r = vendor_1099_report(&l, 2026, None).unwrap();
|
||||
assert_eq!(r.threshold, dec("2000"));
|
||||
let rows: Vec<(&str, Decimal, bool)> = r
|
||||
.vendors
|
||||
@@ -110,14 +110,15 @@ fn cash_paid_per_vendor_and_who_is_reportable() {
|
||||
#[test]
|
||||
fn years_and_thresholds() {
|
||||
let l = books();
|
||||
assert_eq!(default_threshold(2025), dec("600"));
|
||||
assert_eq!(default_threshold(2026), dec("2000"));
|
||||
let y2025 = vendor_1099_report(&l, 2025, None);
|
||||
// SOURCE: rules.md B-19 (sources/irs-1099-thresholds.md).
|
||||
assert_eq!(default_threshold(2025), Ok(dec("600")));
|
||||
assert_eq!(default_threshold(2026), Ok(dec("2000")));
|
||||
let y2025 = vendor_1099_report(&l, 2025, None).unwrap();
|
||||
let dana = y2025.vendors.iter().find(|v| v.name == "Dana").unwrap();
|
||||
assert_eq!(dana.paid, dec("300"));
|
||||
assert!(!dana.reportable, "300 < 600");
|
||||
// An override: at 1,000, Ace's 1,200 is reportable.
|
||||
let r = vendor_1099_report(&l, 2026, Some(dec("1000")));
|
||||
let r = vendor_1099_report(&l, 2026, Some(dec("1000"))).unwrap();
|
||||
assert!(
|
||||
r.vendors
|
||||
.iter()
|
||||
@@ -145,3 +146,60 @@ fn tags_need_real_entries_and_vendors() {
|
||||
assert_eq!(Form1099::parse("misc"), Some(Form1099::Misc));
|
||||
assert_eq!(Form1099::parse("K"), None);
|
||||
}
|
||||
|
||||
/// Spec (Stephen, 2026-10-13): a rule set by law that wanderbooks doesn't
|
||||
/// know for a year is never guessed. 26 U.S.C. §6041(h) indexes the $2,000
|
||||
/// threshold for calendar years after 2026; until the indexed amount is in
|
||||
/// the rule table, asking for it is an error that says what to do.
|
||||
#[test]
|
||||
fn an_unknown_year_threshold_is_never_guessed() {
|
||||
let err = default_threshold(2027).unwrap_err();
|
||||
let LedgerError::RuleNotKnown {
|
||||
rule,
|
||||
year,
|
||||
known_through,
|
||||
spec,
|
||||
} = &err
|
||||
else {
|
||||
panic!("expected RuleNotKnown, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
(rule.as_str(), *year, *known_through),
|
||||
("1099 reporting threshold", 2027, 2026)
|
||||
);
|
||||
assert!(spec.ends_with("irs-1099-thresholds.md"), "{spec}");
|
||||
let why = err.to_string();
|
||||
for needle in [
|
||||
"2027",
|
||||
"official source",
|
||||
"CPA",
|
||||
"github.com/swaits/wanderbooks",
|
||||
] {
|
||||
assert!(why.contains(needle), "missing {needle:?} in: {why}");
|
||||
}
|
||||
let l = books();
|
||||
assert!(matches!(
|
||||
vendor_1099_report(&l, 2027, None),
|
||||
Err(LedgerError::RuleNotKnown { .. })
|
||||
));
|
||||
// An explicit threshold (from the IRS, or the owner or CPA) works.
|
||||
let r = vendor_1099_report(&l, 2027, Some(dec("2100"))).unwrap();
|
||||
assert_eq!(r.threshold, dec("2100"));
|
||||
}
|
||||
|
||||
/// Spec: a dated rule is due for review from its review date, and stays
|
||||
/// due; before it, nothing is stale. The engine has no clock: callers pass
|
||||
/// today.
|
||||
#[test]
|
||||
fn dated_rules_go_stale_on_their_review_date() {
|
||||
use ledger_core::rules::{THRESHOLD_1099, stale_rules};
|
||||
assert!(stale_rules(d(2026, 10, 13)).is_empty());
|
||||
let due = THRESHOLD_1099.review_after();
|
||||
assert!(stale_rules(due.pred_opt().unwrap()).is_empty());
|
||||
assert_eq!(stale_rules(due).len(), 1);
|
||||
assert_eq!(
|
||||
stale_rules(d(2027, 1, 1))[0].name,
|
||||
"1099 reporting threshold"
|
||||
);
|
||||
assert_eq!(stale_rules(d(2030, 6, 1)).len(), 1);
|
||||
}
|
||||
Reference in new issue
Block a user