P3: depreciation straight-line by days through the final month, schedules by day (defects 22, 9, D-37)

The final month takes what the months before leave, spread over its days;
a disposed asset stops on its disposal day. A skipped month is caught up
in the month the run is made; a reversed run un-posts its month. New
schedules spread by day; equal-months schedules must start on the 1st.
Amounts round to the currency's own minor unit. Kani and Lean proofs; an
exhaustive search over runs, reversals and disposals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 14:04:11 -06:00
1 parent 1304b9c0ad
commit b0a9fd8ad4
69 files changed
+10529 -841

No files matched your search

+88 -25
View File
@@ -447,7 +447,14 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
when the shares don't add up to what the entry moved, `parties_error`
says why and confirming fails with code `party_shares`.
- `books.reverse_entry { entry_id, date, description, confirmed }` → same
preview/confirm pattern as post.
preview/confirm pattern as post. A reversed depreciation run or schedule
month is un-posted (its month is due again); refused, at the preview
already (P3): an asset's disposal entry (it would stay disposed), a
depreciation run of a disposed asset (its disposal took it as final),
and a reversal whose reversal would post a depreciation period or a
schedule month a second time (naming the entry that posts it now), or
re-instate a run a later run caught up (carrying the books past the
schedule).
- `books.get_entry { entry_id }` → full entry with lines, receipts, and
its `vendor`, `party_id`, and `party` (the party's label, when it names
exactly one) tags, `parties` / `parties_error` as in the `post_entry`
@@ -556,8 +563,13 @@ every tool over every book in ledger-store's `tests/fixtures/old-books`.
## Tools — fixed assets
Straight-line **book** depreciation only; tax depreciation stays with the
CPA. Partial first/final months prorated by days; the final month trues up
so the lifetime total is exactly (cost − salvage).
CPA. Depreciation accrues by the day (rules.md B-15): a partial first
month, the life's final month (which takes what the others leave, evenly
over its days), and any month a schedule stops in (through a date, or a
disposed asset's disposal day, held) are prorated by days, never plugged;
the lifetime total is exactly (cost − salvage), rounded to the currency's
minor unit. A new asset's cost and salvage are in the minor unit, and its
life's last day is 9999-12-31 at the latest.
- `books.register_asset { description, purchase_date, cost, salvage_value?,
useful_life_months, cost_account, accum_depr_account, expense_account,
@@ -573,19 +585,50 @@ so the lifetime total is exactly (cost − salvage).
tags }`: an Avery 5160/8160 label sheet (PDF): company, tag, QR code of
the tag, description, serial. Default: every active tagged asset; `skip`
reuses a partly used sheet.
- `books.asset_schedule { asset_id | tag }`
→ `{ status, schedule: [{ period, amount, accumulated, book_value }] }`.
- `books.list_assets {}` → assets with status and current book values.
- `books.asset_schedule { asset_id | tag, through? }`
→ `{ status, schedule: [{ period, amount, accumulated, book_value }] }`;
a disposed asset's ends on its disposal day. A schedule an earlier
version's asset can't compute is a tool error saying why, never a
panic.
- `books.list_assets {}` → assets with status, accumulated depreciation
by schedule and book value as of today (a disposed asset's through its
disposal day); `accumulated` and `book_value` are null, with
`schedule_error` saying why, for an asset whose schedule can't be
computed (one bad asset never fails the list).
- `books.run_depreciation { period: "YYYY-MM", asset_id? }`
→ per-asset `{ entry_id, amount, already_posted }`, plus `skipped` for
assets outside their useful life. Idempotent per asset and period.
→ `runs: [{ asset_id, description, period, amount, caught_up, entry_id,
already_posted }]` and `skipped: [{ asset_id, description, reason }]`.
A run posts what the schedule says is due through the month's end, so a
month skipped or reversed is caught up in the month run (`caught_up`:
the part beyond the month's own; a material miss is a restatement for
the CPA, ASC 250), and never more than the schedule at a later month
end. What the books carry is read entry by entry, each in its own
month, so a month whose run was reversed after it ended posts nothing
(it carried the month then; the run of the reversal's month catches it
up). Idempotent per asset and period. Nothing due (the
month rounds to nothing, a later run caught it up, the life is over and
posted) posts nothing and is listed under `skipped` with why, as are a
run-all's assets not yet in service and any whose schedule can't be
computed. A run before the in-service date is refused.
- `books.dispose_asset { asset_id | tag, date, proceeds, proceeds_account,
gain_loss_account, buyer?, confirmed?, receipt_attachment_ids? }` →
without
confirmation: `{ status: "preview", ... }` and posts nothing; with
`confirmed: true`: auto-posts missing final-period depreciation, then
posts the balanced disposal entry. Subject to receipt policy (bill of
sale); monthly depreciation is exempt as an allocation. `buyer` (a
without confirmation: `{ status: "preview", accumulated, book_value,
proceeds, gain_loss, final_depreciation, excess_depreciation,
catch_up_depreciation, lines, receipt_required }` and posts nothing;
with `confirmed: true`: auto-posts the final depreciation (through the
disposal day, held), then posts the balanced disposal entry. Book value
is cost less the schedule through the disposal day, whatever the runs
posted: depreciation a run posted past it (the disposal month's run is
dated its end) is credited back to depreciation expense in the disposal
entry (`excess_depreciation`); depreciation still due when the disposal
month already has a run is debited there (`catch_up_depreciation`); the
entry clears everything the runs carry. Refused, at the preview already,
unless every run carries at the end of the disposal's month what it
carries now: a run of a later month that stands (reverse it first), or
a reversal (or re-instatement) of an earlier run dated after the
disposal's month that a later one hasn't undone (undo it, on any open
day). Subject to receipt policy (bill
of sale); monthly depreciation is exempt as an allocation. `buyer` (a
party) is the disposal entry's party; under `[books] parties =
"require"` a sale with proceeds without one is refused before anything
posts (`party_required`); a retirement with no proceeds and no buyer is
@@ -603,27 +646,47 @@ so the lifetime total is exactly (cost − salvage).
## Tools — schedules and recurring entries
Amortization spreads a prepaid expense (asset → expense) or deferred
revenue (liability → revenue) evenly over the months it covers, posting on
each month end, to the cent (the last month absorbs rounding). Like
revenue (liability → revenue) over the time it covers, posting on each
month end, to the minor unit (rules.md B-16). A new schedule's convention
is `books.schedule_convention` (unset: `daily`, D-11/D-37): `daily`
spreads by the day from `start`, the first day covered, as depreciation
does, so a mid-month start posts a small first and last month (a 12-month
policy from March 15 is thirteen month ends: 54.84, eleven of 100.00,
45.16 for 1,200.00); `monthly_even` is equal months, the last absorbing
rounding, and starts on the 1st (D-37: revenue is never earned early,
ASC 606-10-25-23). Schedules registered before P3 stay `monthly_even`
(mid-month starts included), forever. Like
depreciation, the schedule is the evidence: its postings are exempt from
the receipt policy (the original payment carries the receipt). Recurring
entries never post: they generate drafts, so every real receipt still goes
through the policy.
- `books.create_schedule { kind: "prepaid_expense" | "deferred_revenue",
description, total, start, months, balance_account, pl_account }` →
`{ schedule_id, ..., total, remaining, months: [{ date, amount, posted }],
plan_error }`. Prepaid needs an asset and an expense account; deferred
revenue a liability and a revenue account. `plan_error` is null, except
for a schedule an earlier version accepted whose months run past the
last date the books can compute: then it says why, `remaining` is null
and `months` empty, and `books.run_schedules` refuses until it is dealt
with.
description, total, start, months, balance_account, pl_account,
convention?: "daily" | "monthly_even" }` → `{ schedule_id, ...,
convention, start, end, next, total, remaining, months: [{ date, amount,
posted }], plan_error }`. Prepaid needs an asset and an expense account;
deferred revenue a liability and a revenue account. `start` is the
first day covered (for a monthly_even schedule an earlier version took
mid-month, the 1st of its month), `end` the last day covered, `next`
the next month end to post (null once every month has). Refused: a
convention but those two (naming them); a `monthly_even` schedule not
starting on the 1st (naming the 1st, and `daily`); one whose months
aren't whole minor units (yen); a daily one whose last day is after
9999-12-31. `plan_error` is null, except for a schedule an earlier
version accepted whose months run past the last date the books can
compute: then it says why, `remaining` and `next` are null and `months`
empty, and `books.run_schedules` refuses until it is dealt with.
- `books.list_schedules {}` → `{ schedules: [...] }`, same shape.
- `books.run_schedules { through?, confirmed? }` → `{ status: "preview" |
"posted", postings: [{ schedule_id, date, description, amount,
entry_id? }] }`: every unposted month ending on or before `through`
(default today). Idempotent; closed periods are refused.
entry_id? }], skipped: [{ schedule_id, date, reason }] }`: every
unposted month ending on or before `through` (default today).
Idempotent; a month reversed within its month is due again; a month
dated in a closed period, or whose posting was reversed after the month
ended (posting it again would recognize it twice until then), is left
and listed under `skipped` with why (book it in an open period by hand,
or not at all), never a failed run.
- `books.create_recurring { description, frequency: "weekly" | "monthly" |
"quarterly" | "yearly", start, end?, lines: [{ account, side, amount }] }`
→ `{ recurring_id, ..., generated: [] }`. Lines must balance. Monthly and
+191 -10
View File
@@ -71,7 +71,12 @@ minor unit), `Ledger::add_line_fx` (base amount = foreign amount × rate,
rounded to the minor unit; original kept), `Ledger::post_entry` (every
line, whatever made it: payroll, closes, disposals; `FinerThanMinorUnit`,
since 2026-10-13 after a payroll run posted $65.27794). Journals from
before replay as booked (`replay_post_entry`). **Checked:**
before replay as booked (`replay_post_entry`). A new asset's cost and
salvage are in the minor unit too (P3: every depreciation and disposal
line derives from them; `validate_new_asset`), and depreciation and daily
schedules round to it (B-15, B-16); assets an earlier version took finer
replay as journaled. **Checked:**
`an_asset_cost_finer_than_a_cent_is_refused`,
`a_foreign_line_is_booked_in_whole_base_units`,
`no_entry_posts_a_fraction_of_the_minor_unit`; the GnuCash oracle.
**Checked:** `foreign_currency_line_is_rejected`, `fx_overflow_is_an_error`.
@@ -679,9 +684,127 @@ allocation.
Tax conventions (half-year, mid-month, mid-quarter): IRS Pub. 946
(`sources/irs-depreciation.md`), for the CPA.
**Enforced:** `assets::depreciation_schedule`. **Checked:**
The rule, exactly (P3, defect 22; the engineer settled it from ASC
360-10-35-4, 2026-10-20):
1. The life is `[in-service date, in-service date + life months)` (chrono
clamps a day past a month's end: Jan 31 plus one month is Feb 28), cut
into the calendar months it touches.
2. Through day `k` of a month but the life's last, the depreciation is
(cost − salvage) / life × (the months before, each as days held over
days in the month, + `k` / days in this month): the monthly rate by
days.
3. The life's last month takes what the months before it leave, evenly
over its days: through its last day the depreciation is exactly cost
− salvage, and through an earlier day of it, prorated by days, never
plugged to the whole. (Without clamping its daily rate is the first
month's: the remainder is that month's missing days.) Before P3 any
schedule through a day of the final month, and every disposed asset's,
plugged to the whole: a disposal in the final month posted too much
depreciation and too large a gain.
4. Each cumulative figure is rounded half away from zero to the
currency's minor unit (cents; whole yen; a thousandth of a dinar;
ISO 4217, `sources/iso-4217.md`), in exact integer arithmetic
(`kernel::prorated_through`); a month's amount is the difference of
consecutive ones. Through every month end this is the per-month
running total books posted before P3, except where the exact figure is
a half-cent tie, which the old 28-digit Decimal quotients could round
either way; P3 rounds it up, exactly, moving that cent to the month
before.
5. A disposed asset depreciates through its disposal day, held
(inclusive): its schedule, accumulated depreciation and book value
stop there, and equal what the disposal took. The disposal's book
value is cost less the schedule through that day whatever the runs
posted: depreciation still due posts as the final depreciation;
depreciation the disposal month's run (dated its end) posted past the
day is credited back to depreciation expense in the disposal entry
(`excess_depreciation`); and the disposal clears what the runs carry,
so the asset leaves the accumulated depreciation account at zero. A
disposal is refused unless every run carries at the end of the
disposal's month what it carries now (the disposal clears what they
carry now): while a run of a later month stands (reverse it first:
credited back at the disposal day, it would leave the disposal month's
accumulated depreciation a debit and its expense short), or while a
reversal (or re-instatement) of an earlier run dated after the month
isn't undone (the disposal would charge that run again, and the
reversal take it away only later; undone on any open day, so a closed
month never blocks a disposal for ever). A change a run carries only
at later month ends (a later month's run reversed after its month) is
that run's error and its correction, dated when made, and the disposal
leaves it as it is.
6. A month's run posts what the schedule says is due through the
month's end, less what the books carry then, but never so much that
the books would carry more than the schedule at a later month end
(`kernel::catch_up`). What the books carry is read entry by entry, each
in its own month (`assets::depreciation_effects`): a run adds its
amount in its month, a reversal of it takes it away in the reversal's
month. So a month skipped, or a run reversed, is caught up in the month
the run is made (plan/served.md P3, settled in design, 2026-10-20: an
immaterial miss; a material one is restated under ASC 250, a
correction for a human and the CPA), and a month whose run was
reversed after it ended is never run twice (it carried the month at
its end; the run of the reversal's month catches it up). A month a
later run caught up, or that rounds to nothing, posts nothing (an entry
carries no zero line); a run after the life posts what is still due.
A reversed run is un-posted (its month due again, when reversed within
it); re-instating one (reversing its reversal) is refused when it would
carry the books past the schedule (a later run caught its month up:
`assets::catch_up_room`); a disposal, or a run its disposal took as
final, can't be reversed; and a run, or a reversal of one, can't be
reversed on a day before its own (a correction comes after what it
corrects, B-9: a run of March reversed on Jan 31 would leave January
and February with negative depreciation; commands only, journals
replay as written). The viewer and the store read every run the
journal holds, not a month's latest (a run re-instated after its month
was run again and that run reversed is the month's run).
**Enforced:** `assets::depreciation_schedule`, `assets::run_amount`,
`assets::catch_up_room`, `assets::depreciation_effects`,
`kernel::prorated_through`, `kernel::catch_up`, `kernel::true_up`,
`calendar::month_parts`, `Store::run_depreciation`,
`Store::dispose_preview` (`check_nothing_after_disposal`),
`Store::check_reverse_entry`. **Checked:**
`schedule_sums_exactly_to_depreciable_amount`,
`salvage_value_is_never_depreciated`, `prop_depreciation_schedule_exact`.
`salvage_value_is_never_depreciated`, `prop_depreciation_schedule_exact`,
`defect22_depreciation_through_mid_final_month_is_prorated_not_plugged`,
`defect22_final_month_drift_is_spread_by_days`,
`defect22_a_disposed_assets_schedule_stops_at_its_disposal_day`,
`defect22_a_disposal_in_the_final_month_posts_depreciation_by_days`,
`defect22_a_disposal_inside_a_posted_month_takes_back_the_extra_days`,
`a_run_reinstated_after_its_rerun_was_reversed_still_counts`,
`the_viewer_counts_a_run_reinstated_after_its_rerun_was_reversed`,
`a_reversal_dated_before_the_entry_it_reverses_is_refused`,
`a_disposal_is_taken_once_a_late_change_is_undone_after_its_month_closed`,
`a_disposal_whose_month_has_run_charges_what_is_still_due_in_its_entry`,
`every_order_of_runs_reversals_disposals_and_closes_keeps_the_books_right`,
`prop_any_sequence_keeps_the_books_right`,
`prop_the_viewer_reads_an_asset_as_the_store_does`,
`prop_run_amounts_are_each_months_run_amount`,
`jpy_depreciation_posts_whole_yen`, `a_skipped_month_is_caught_up_in_the_month_run`,
`a_reversed_depreciation_month_can_be_run_again`,
`a_run_reversed_after_its_month_is_caught_up_in_the_reversals_month`,
`reinstating_a_run_a_later_run_caught_up_is_refused`,
`a_disposal_before_a_later_months_run_is_refused_until_it_is_reversed`,
`a_disposal_before_a_later_reversal_of_an_earlier_run_is_refused`,
`a_disposal_entry_cannot_be_reversed`,
`prop_the_final_month_takes_the_remainder_by_its_days`,
`depreciation_effects_read_each_entry_in_its_month`,
`prop_depreciation_is_monotone_and_bounded`,
`prop_disposed_schedule_equals_what_disposal_computed`,
`prop_month_end_schedules_match_the_legacy_arithmetic_except_half_cent_ties`;
`the_charge_never_falls_and_is_continuous_over_real_calendars`; Kani
`prorate_parts_sum_to_total`, `prorate_last_part_absorbs_the_remainder`,
`a_months_charge_is_within_half_a_cent`,
`a_months_charge_in_tenths_is_within_half_a_cent`,
`the_last_months_charge_is_within_half_a_cent`,
`the_last_months_charge_in_tenths_is_within_half_a_cent`,
`prorated_through_refuses_malformed_input`,
`catch_up_never_overtakes_the_schedule`,
`catch_ups_is_every_suffixs_catch_up`, `true_up_reaches_the_target`;
Lean `Prorate.kernel_parts_total`, `Prorate.kernel_parts_nonneg`,
`Prorate.part_near`, `Prorate.cum_near`, `Prorate.chargeMid_eq_cum`,
`Prorate.chargeLast_eq_cum`, `Prorate.chargeLast_start`,
`Prorate.chargeLast_mono`, `Prorate.cum_mono` (`docs/verification.md`).
## B-16 Prepaid expenses and deferred revenue are recognized over the period benefited or earned
@@ -697,8 +820,51 @@ current within the operating cycle; the ASC prescribes no expensing
method, so monthly expensing stays a convention. Tax: the 12-month rule, Treas. Reg. §1.263(a)-4(f)
(`sources/irs-capitalization.md`).
**Enforced:** `schedules`. **Checked:** `a_prepaid_expense_is_expensed_month_by_month`,
`deferred_revenue_is_earned_to_the_cent`.
Conventions (P3, defect 9): `daily` or `monthly_even`. A new schedule
spreads by the day from its start (the first day covered), as
depreciation does (B-15 rules 1-4: full months equal, the first and last
partial months by their days, the last taking what the others leave,
rounded to the minor unit): a 12-month policy from March 15 posts
thirteen month ends, a small March and a small final March (DECISION,
the owner, D-11 confirmed in D-37, 2026-10-20). Schedules registered before
P3 keep equal months, the last absorbing rounding, forever: a posted
month's replay re-derives its plan. `books.schedule_convention` sets the
convention new schedules take (unset: daily). An equal-months schedule
starts on the 1st of a month, for prepaid expenses and deferred revenue
alike (DECISION, the owner, D-37, 2026-10-20): revenue is never recognized
before it is earned (ASC 606-10-25-23), and one rule for both is simpler
and never early; schedules an earlier version took mid-month replay as
they were. A reversed month is un-posted, so it is due again; a month in
a closed period, or one whose posting was reversed after the month ended
(posting it again on its own date would recognize it twice until the
reversal; a schedule posts its plan's months, so it can't catch one up
later), is left by a run for a human, listed with why, never a failed run.
A month's posting can't be reversed on a day before its own (a
correction comes after what it corrects, B-9; commands only). Not yet
vendored: ASC 606-10-25-31 (progress toward satisfying an obligation over
time), 606-10-25-32 (one method, applied consistently), 606-10-55-20 (time
elapsed as an input method): to read from the owner's ASC login before
B-16 cites them (P3 depreciation review R7).
**Enforced:** `schedules` (`daily_plan`, `amortization_plan`,
`validate_schedule`), `kernel::prorate_by_days`, `Ledger::reverse_entry`,
`Ledger::schedule_month_hold`.
**Checked:** `a_prepaid_expense_is_expensed_month_by_month`,
`deferred_revenue_is_earned_to_the_cent`,
`defect9_a_new_schedule_spreads_by_the_day`,
`defect9_deferred_revenue_daily_is_earned_to_the_cent`,
`defect9_legacy_schedules_stay_monthly_even`,
`an_equal_months_schedule_starts_on_the_first`,
`a_jpy_daily_schedule_posts_whole_yen`, `a_kwd_daily_schedule_rounds_to_the_fils`,
`a_reversed_schedule_month_is_due_again`,
`a_reversal_dated_before_the_entry_it_reverses_is_refused`,
`a_schedule_month_reversed_after_it_ended_is_left_for_a_human`,
`a_month_that_rounds_to_nothing_is_never_a_posting`,
`a_schedule_month_posted_again_declares_month_reposting_and_replays`,
`daily_plans_pin_month_end_clamping`,
`prop_daily_plan_sums_exactly_and_lands_on_month_ends`,
`prop_monthly_even_plan_is_unchanged`; `ledger-mcp`
`mcp_daily_schedule_over_stdio`.
## B-17 Platform payouts are recorded gross when the business is the principal
@@ -1258,13 +1424,23 @@ count's first close, `"rounding": "cumulative_since"`), `account_renamed`
(before the first `settings.set` naming `policy.require_tax_category`,
B-32), `party_creditors` (`party.creditor_set`: defect 24, B-35),
`payment_instruments` (`instrument.added`, `entry.instrument_set`:
defect 12, B-19) and `vendor_1099_decisions` (`vendor.1099_set` with who
defect 12, B-19), `vendor_1099_decisions` (`vendor.1099_set` with who
decided, a `not_reportable` decision, its reason or the date it takes
effect, and `vendor.w9_flag_set`: defects 18 and 23, D-40, B-19). A value
effect, and `vendor.w9_flag_set`: defects 18 and 23, D-40, B-19),
`schedule_convention` (a daily schedule's `schedule.registered` carries
`convention: daily`; an older version would read it as equal months,
B-16) and `month_reposting` (a schedule month or a depreciation period
posted again after a reversal un-posted it; an older version would refuse
the second `schedule.posted`, and its projection ignore the second
`asset.depreciation_posted`, B-15). A value
in a line of a known type that only a newer version writes (a 1099
decision or reason kind, a payment method's kind) is newer, never
corrupt. A later piece asks the owner's go-ahead before the first such
declaration (plan D-39).
corrupt. A command decides whether it needs a feature, and declares it,
before its first journaled line, so a feature refused (a format upgrade
awaiting the owner's consent) journals nothing, never an entry without
the line that says what it is
(`a_refused_journal_feature_journals_nothing`). A later piece asks the
owner's go-ahead before the first such declaration (plan D-39).
**Enforced:** `ledger_store::format` (`JournalFormat::admit`, `declared`,
`gate`, `settle`, `declaration`), `journal::ENVELOPE_KEYS`,
@@ -1299,7 +1475,12 @@ bounds are the commands', never replay's)
0001-01-01 through 9999-12-31 (`ledger_core::calendar`): a date given
to a tool is exactly YYYY-MM-DD, a real day in that range (no sign, no
space, no fifth year digit); a period is a month of it; a new schedule's
last month, a new asset's last day of depreciation, a new recurring
last month (a daily one's last day), a new asset's last day of
depreciation (so a life ending on 9999-12-31 is taken, and one a day
later refused: P3 settled the plan's defect 22 question this way, lives
past 9999 refused at registration, the bound the life's last day held;
ones an earlier version took open, and their months after 9999-12 show
as scheduled and never post), a new recurring
entry's start and end, a new period, a new invoice's or bill's date and
due date, a disposal's date stay in it. Every amount a command takes,
not only a posted line's, is at most a journal line's
+82
View File
@@ -1368,6 +1368,88 @@ unknown-code check, the accounts passed) and `update_party`'s feature
declaration; the bookspec loader's party, payee, decision and payment
method setup.
## P3: depreciation and schedules (defects 22 and 9, 2026-10-20)
Every expected figure below was worked by hand from the rule (rules.md
B-15, B-16) and is written out in the test's doc comment; the fixtures are
made up (asset X: 12,000.00 over 12 months from 2026-01-01; a 1,200.00
policy from March 15). Each regression that is an accounting rule is also
a bookspec example (`bookspec/examples/`, P13, D-34).
| Law | Test |
|---|---|
| Through a day of the life's final month, depreciation is prorated by days, never plugged (repro A: December 483.87, accumulated 11,483.87) | `ledger-core` `spec_assets_payroll.rs` `defect22_depreciation_through_mid_final_month_is_prorated_not_plugged`; `spec_asset_tags.rs` `register_as_of_a_day_before_a_same_month_disposal_is_prorated` |
| A mid-month life's drift is spread over the final month's days (repro D: through Apr 13, 920.00; Apr 14, 930.00) | `defect22_final_month_drift_is_spread_by_days`; `spec_replayed_extremes.rs` `depreciation_reaches_chronos_last_month` (1,185.92 through Dec 10 of year 262142) |
| A disposed asset's schedule stops at its disposal day, held, and equals what the disposal took (repro B) | `defect22_a_disposed_assets_schedule_stops_at_its_disposal_day`, property `prop_disposed_schedule_equals_what_disposal_computed`; `ledger-mcp` `spec_assets.rs` `a_disposed_asset_lists_what_it_depreciated_through_its_disposal` |
| A disposal in the final month posts depreciation by days (repro C: 322.58, gain 322.58) | `ledger-store` `tests/assets.rs` `defect22_a_disposal_in_the_final_month_posts_depreciation_by_days` |
| A disposal inside a month already run takes back the days after it (R2: excess 500.00, book value 6,500.00) | `defect22_a_disposal_inside_a_posted_month_takes_back_the_extra_days` (store and `ledger-mcp` `spec_assets.rs`) |
| A month skipped is caught up in the month the run is made; a later run for it posts nothing (settled in design, P3) | core `a_skipped_month_is_caught_up_in_the_month_run`, store `a_skipped_month_is_caught_up_in_the_month_run`, MCP `a_skipped_month_is_caught_up_and_said` |
| A reversed run is un-posted and runs again (feature month_reposting); re-instating a run whose month ran again is refused; a disposal, and a run its disposal took as final, can't be reversed; a reversed run before a disposal leaves nothing behind (R3) | store `a_reversed_depreciation_month_can_be_run_again`, `a_disposal_entry_cannot_be_reversed`, `a_reversed_run_before_a_disposal_leaves_nothing_behind`; core `a_reversed_schedule_month_is_due_again` |
| What the books carry is read entry by entry, each in its own month: a run reversed after its month ended isn't run twice (its month posts nothing; the reversal's month catches it up), for depreciation; a schedule month reversed after it ended is left for a human, listed with why (P3 review) | store `a_run_reversed_after_its_month_is_caught_up_in_the_reversals_month`; GUI `a_run_reversed_after_its_month_leaves_the_reversals_month_due`; core `schedules.rs` `a_schedule_month_reversed_after_it_ended_is_left_for_a_human`; store `format.rs` `a_schedule_month_posted_again_declares_month_reposting_and_replays` |
| Re-instating a run (reversing its reversal) never carries the books past the schedule, nor past cost less salvage (P3 review) | store `reinstating_a_run_a_later_run_caught_up_is_refused` |
| What the books carry is each run and each reversal in its own month, alternately adding and taking away (P3 review) | core `spec_assets_payroll.rs` `depreciation_effects_read_each_entry_in_its_month` |
| A schedule month that rounds to nothing is never a posting: left out of a daily plan, never due in an equal-months one (P3 review; no test held it before) | core `schedules.rs` `a_month_that_rounds_to_nothing_is_never_a_posting` |
| A disposal is refused while a later month's run stands, or while an earlier run's reversal is dated after the disposal's month; once undone, the disposal month ends at 0.00 accumulated and its expense whole (P3 review) | store `a_disposal_before_a_later_months_run_is_refused_until_it_is_reversed`, `a_disposal_before_a_later_reversal_of_an_earlier_run_is_refused` |
| A disposed asset's figures in the viewer are what its disposal took (its excess or catch-up from the disposal entry) and none of its months is Due (P3 review) | GUI `a_disposed_assets_figures_are_what_its_disposal_took` |
| The final month is C + (D − C) × k / d, rounded half up, against arithmetic written in the test (P3 review) | property `prop_the_final_month_takes_the_remainder_by_its_days` (500 cases) |
| A month that rounds to nothing posts and journals nothing (R10) | store `run_depreciation_posts_nothing_for_a_month_that_rounds_to_nothing`, MCP `run_depreciation_skips_a_month_that_rounds_to_nothing` |
| Depreciation and daily schedules round to the currency's minor unit (R1: yen, dinars) | `jpy_depreciation_posts_whole_yen`, store `jpy_depreciation_runs_post_whole_yen`, `schedules.rs` `a_jpy_daily_schedule_posts_whole_yen`, `a_kwd_daily_schedule_rounds_to_the_fils`; `money.rs` `steps_are_the_currencys_minor_unit` |
| A new schedule spreads by the day (1,200.00 from March 15: 54.84, eleven 100.00, 45.16); deferred revenue is earned by the day (290.32, 333.34, 333.33, 43.01) | `schedules.rs` `defect9_a_new_schedule_spreads_by_the_day`, `defect9_deferred_revenue_daily_is_earned_to_the_cent`; `schedules.rs` unit `a_daily_plan_spreads_by_the_day`; `ledger-mcp` `mcp_stdio.rs` `mcp_daily_schedule_over_stdio` |
| Schedules from before P3 stay equal months, forever | `defect9_legacy_schedules_stay_monthly_even`, property `prop_monthly_even_plan_is_unchanged`; every old-books fixture (`books_an_earlier_version_wrote_open_to_its_figures`) |
| An equal-months schedule starts on the 1st (D-37); a daily one's last day is in the calendar | `an_equal_months_schedule_starts_on_the_first`, `spec_bounds.rs` `a_daily_schedule_whose_last_day_passes_9999_is_refused` |
| chrono's month-end clamping is pinned for daily plans (Jan 29, 30, 31; Feb 29) | `schedules.rs` unit `daily_plans_pin_month_end_clamping` |
| A new asset's cost and salvage are in the minor unit (replay takes finer ones, computed); a life ending 9999-12-31 is taken, a day later refused | `spec_bounds.rs` `an_asset_cost_finer_than_a_cent_is_refused`, `a_life_ending_on_9999_12_31_is_accepted`, `a_life_ending_after_it_is_refused` |
| The first daily schedule declares feature schedule_convention; an older reader stops there; monthly_even declares nothing; an unknown convention is newer, never corrupt | `ledger-store` `tests/format.rs` `the_first_daily_schedule_declares_its_feature_and_an_older_reader_stops_there`, `a_monthly_even_schedule_declares_no_feature`, `an_unknown_schedule_convention_is_newer_not_corrupt`; `format.rs` unit `an_older_reader_stops_at_p3s_features` |
| Accumulated depreciation never falls, never exceeds cost − salvage, is 0 before service and the whole from the last day (sampled days: the first and final months, two between, a dozen month ends; R9) | property `prop_depreciation_is_monotone_and_bounded` (500 cases) |
| No posted month moves but a half-cent tie's cent (old Decimal arithmetic as the oracle) | property `prop_month_end_schedules_match_the_legacy_arithmetic_except_half_cent_ties` (500 cases) |
| A daily plan sums exactly, lands on month ends, each amount > 0; from the 1st, `months` postings within a cent of total/months | property `prop_daily_plan_sums_exactly_and_lands_on_month_ends` (2,000 cases) |
| Refused commands never reach the journal, now with daily and monthly_even schedules and zero-month runs | `ledger-store` `tests/store.rs` `refused_commands_never_reach_the_journal` |
| Every order of runs, reversals (any day, before an entry's own included), re-runs, re-instatements, disposals and closes on one asset keeps the books right: refusals journal nothing; at every month end before the disposal the expense is the accumulated depreciation and within the schedule; one run in effect per month; a disposal's month and the end at the schedule through its day, accumulated 0.00, the gain proceeds less book value; an active asset run month by month past its life takes exactly cost less salvage (P3 review round 2) | store `asset_orders.rs` `every_order_of_runs_reversals_disposals_and_closes_keeps_the_books_right` (exhaustive, three commands deep; four and five with `WB_ORDERS_DEPTH`, run clean 2026-10-21), `prop_any_sequence_keeps_the_books_right` (64 cases, up to 9 commands, any day) |
| The viewer reads an asset as the store does, whatever the runs, reversals and disposals: what the books carry, each finished month's status against a run of it, what is unposted (P3 review round 2) | GUI `prop_the_viewer_reads_an_asset_as_the_store_does` (48 cases), `the_viewer_counts_a_run_reinstated_after_its_rerun_was_reversed` |
| A run re-instated after its month was run again and that run reversed still counts: in the store, the disposal and the viewer (P3 review round 2; before, the projection's row named the reversed re-run) | store `a_run_reinstated_after_its_rerun_was_reversed_still_counts`; GUI `the_viewer_counts_a_run_reinstated_after_its_rerun_was_reversed` |
| A depreciation run's or schedule month's reversal can't be dated before the entry it reverses (P3 review round 2: a run of March reversed on Jan 31, then a disposal on Feb 15, ended February with a 1,000.00 debit in accumulated depreciation) | store `a_reversal_dated_before_the_entry_it_reverses_is_refused` |
| A disposal waits only until what the runs carry at its month's end is what they carry now, so a late change undone after its month closed lets it through (P3 review round 2; round 1 refused it for ever) | store `a_disposal_is_taken_once_a_late_change_is_undone_after_its_month_closed` |
| Depreciation still due when the disposal's month has a run is charged in the disposal entry, never as a second run of the month (P3 review round 2; no store test held it) | store `a_disposal_whose_month_has_run_charges_what_is_still_due_in_its_entry` |
| A command that needs a journal feature decides and declares it before its first journaled line: a refused feature journals nothing (the format-upgrade gate) | store `format.rs` `a_refused_journal_feature_journals_nothing` |
| What a viewer's one pass says a run of each month posts is run_amount's for that month | property `prop_run_amounts_are_each_months_run_amount` (500 cases); Kani `catch_ups_is_every_suffixs_catch_up` |
| The kernel on i64 and i32 (Kani's) is the kernel on i128 and Decimal (the engine's) | `kernel_bridge.rs` `prorate_agrees_at_i64_and_i128`, `prorated_through_agrees_at_i32_and_i128`, `catch_up_and_true_up_agree` |
| The charge never falls and is continuous at month ends, over every 1-to-3-month span from every day of a common and a leap year | `kernel.rs` unit `the_charge_never_falls_and_is_continuous_over_real_calendars` (exhaustive) |
| Proofs | Kani `prorate_parts_sum_to_total`, `prorate_last_part_absorbs_the_remainder`, `a_months_charge_is_within_half_a_cent`, `a_months_charge_in_tenths_is_within_half_a_cent`, `the_last_months_charge_is_within_half_a_cent`, `the_last_months_charge_in_tenths_is_within_half_a_cent`, `prorated_through_refuses_malformed_input`, `catch_up_never_overtakes_the_schedule`, `catch_ups_is_every_suffixs_catch_up`, `true_up_reaches_the_target`; Lean `Prorate.lean` (`cum_mono`, `cum_le`, `cum_full`, `parts_telescope`, `prorate_total`, `prorate_nonneg`, `part_within_a_unit`, `roundUnit_near`, `roundUnit_scale`, `cum_near`, `part_near`, `chargeMid_eq_cum`, `chargeLast_eq_cum`, `chargeLast_start`, `chargeLast_mono`, `kernel_parts_total`, `kernel_parts_nonneg`; `docs/verification.md`) |
**Mutation targets for P12** (none run in P3, D-33): `kernel.rs`
`prorated_through` (the last/not-last branch, the rounding comparison
`r >= step - r` against `>`, the `k == days && last` shortcut, the cap at
the total, every refusal), `round_to_unit`, `prorate_by_days`,
`part_weight`, `MonthPart::is_valid`, the three `Units` impls, `catch_up`,
`catch_ups`, `true_up`; `calendar.rs` `month_parts`, `days_in_month`,
`next_month_start`; `money.rs` `Steps::of`, `Steps::decimal`; `assets.rs`
`schedule_months` (the stop date's `min` with the disposal day, the
inclusive `+ 1`, the partial-month break), `life_end`, `check_disposal`,
`run_amount`, `catch_up_room`, `depreciation_effects` (the chain's
alternating signs), `run_amounts` (its stop past the life), `cumulatives`;
`ledger.rs` `check_new_asset` (the minor-unit checks, the `pred()`
bound), `check_reversal` SCENARIO 5, `in_effect`, `reverse_entry_steps`'
schedule un-posting, `schedule_month_posted_before`,
`schedule_month_reversed_after`, `schedule_month_hold`; `schedules.rs`
`daily_plan`, `plan_for`, `validate_schedule` (the 1st rule, the daily
bound, the minor-unit plan check), `first_day`, `last_day`, `next`, `due`'s
zero months, `ScheduleConvention::parse`/`key`; `settings.rs` the new key
and `schedule_convention()`'s default; `store.rs` `run_depreciation`
(nothing due, the feature on a re-run), `live_runs`,
`depreciation_runs_ever`, `dispose_preview` (true-up split, `in_month`),
`check_nothing_after_disposal`, `disposal_lines`, `check_reverse_entry`
(the re-instatement room),
`register_schedule` (the feature only for daily), `ensure_feature`,
`post_schedule_month`'s feature, `run_schedules`' held months, the
`schedule.registered` replay arm's convention, the `asset.depreciation_posted`
projection upsert, `period_month`; `format.rs` `KNOWN_FEATURES`,
`declared_knowing`; `server.rs` `create_schedule`'s default resolution,
`schedule_out`'s new fields, `run_depreciation`'s skips, `list_assets`'
`schedule_error`, `dispose_asset`'s debits, `run_schedules`' held months;
`ledger-gui` `books.rs` `disposal_true_up`, `pages/assets.rs` `unposted`
and `schedule`'s disposed months. Equivalent mutants foreseen:
none (`r >= step - r` against `>` is killed by `half_a_unit_rounds_up`).
## Benchmarks (baseline 2026-10-13)
No budget, by decision (the owner, 2026-10-13: the books do little work
+133 -14
View File
@@ -6,9 +6,9 @@ ways, each covering what the others can't:
| Layer | Tool | What it proves | Scope |
|---|---|---|---|
| **Code** | [Kani](https://model-checking.github.io/kani/) (bounded model checking) | The engine's own Rust kernel is correct and never panics or overflows | Every input within the bounds: up to 4 lines, 3 accounts, amounts to 2^40 |
| **Code** | [Kani](https://model-checking.github.io/kani/) (bounded model checking) | The engine's own Rust kernel is correct and never panics or overflows | Every input within the bounds: up to 4 lines, 3 accounts, amounts to 2^40; proration by days up to 3 month parts (bounds below) |
| **Laws** | [Lean 4](https://lean-lang.org) (machine-checked proof) | The laws hold for books of any size, after any sequence of commands | Unbounded: any number of entries, accounts, periods, steps |
| **Bridge** | proptest | The kernel on `Decimal` (what the engine runs) agrees with the kernel on `i64` (what Kani proves) | 2,000 random cases per property |
| **Bridge** | proptest | The kernel on `Decimal` (what the engine runs) agrees with the kernel on `i64` (what Kani proves); proration's `i128` (the engine's) with its `i64` and `i32` | 2,000 random cases per property |
Run all of it with `just prove` (after `mise install`; see the end).
@@ -26,12 +26,14 @@ period closing, `periods::allocate`, and the trial balance. There is no
second copy of the arithmetic for proofs to drift from.
Kani compiles the same functions at `i64` (exact integer money in the
smallest unit) and explores every input in the bounds. (Grouping by
smallest unit; the four proration charge harnesses at `i32`) and
explores every input in the bounds. (Grouping by
account is proved through `movements_into`, the allocation-free core
that `movements` wraps: symbolic heap growth is beyond what the model
checker explores in reasonable time.) The Lean model in
`verify/lean` restates each function over `Int` and proves the laws with
no bounds at all.
`verify/lean` restates each function over `Int` (proration over `Nat`,
both of `prorated_through`'s branches) and proves the laws with no bounds
at all.
## The laws
@@ -306,6 +308,117 @@ their numbers are assigned when P3 lands:
`a_reversal_nets_every_bucket_to_zero` (ledger-core
`tests/form1099.rs`)
### Proration by days, catching up, truing up (P3, defects 22 and 9)
Depreciation (rules.md B-15) and daily prepaid and deferred-revenue
schedules (B-16) spread a total over a span of days with one kernel
function, `kernel::prorate_by_days` (each month's share through
`kernel::prorated_through`), in exact integer arithmetic over a common
unit, `MONTH_L` = lcm(28, 29, 30, 31) = 377,580 parts of a month, so a day
of any month is a whole number of them.
22. **The parts of a span sum to its total exactly, none is negative, and
the last is the total less the others** (plan/served.md P3 defect 9:
"parts sum to the total; the last part absorbs the remainder"). Every
split whose parts but the last fit in the span is computed; only parts
no calendar has are refused.
- Kani `prorate_parts_sum_to_total`, `prorate_last_part_absorbs_the_remainder` (a
corollary, kept because the plan names the law) · Lean
`Prorate.kernel_parts_total`, `Prorate.kernel_parts_nonneg` (the
kernel's split, any number of parts and any sizes), `Prorate.cum_dvd`
(a total in whole units is charged, so split, in whole units), from
`Prorate.prorate_total`, `Prorate.prorate_nonneg`,
`Prorate.parts_telescope` · bridge `prorate_agrees_at_i64_and_i128`
23. **Each part but the last is its days times the monthly rate within one
minor unit** (P3 defect 9: "each part equals days × rate within 0.01",
stated for any currency's minor unit). Each cumulative charge is its
exact share rounded half up to a multiple of the unit (a tie goes up;
the total itself when that rounds above it): Kani of the kernel at its
bounds, Lean of both of the kernel's branches at any size
(`roundUnit_near`, `cum_near`; `chargeMid_eq_cum`, `chargeLast_eq_cum`
say the kernel's two formulas are the model's). Two such halves make
the part's one unit (`part_near`, at any size, for any parts).
- Kani `a_months_charge_is_within_half_a_cent`,
`a_months_charge_in_tenths_is_within_half_a_cent`,
`the_last_months_charge_is_within_half_a_cent`,
`the_last_months_charge_in_tenths_is_within_half_a_cent` · Lean
`Prorate.roundUnit_near`, `Prorate.cum_near`, `Prorate.part_near`,
`Prorate.part_within_a_unit` · bridge `prorated_through_agrees_at_i32_and_i128`
24. **The charge through a day never falls, never exceeds the total, is
continuous where one month ends and the next begins, and is the total
on the last day**; malformed input (a negative total, no months, a
unit that isn't positive, a part no month has, a day past the part, a
weight outside the span) is refused, never a panic.
- Lean `Prorate.cum_mono`, `Prorate.cum_le`, `Prorate.cum_full` (the
model, any span), `Prorate.chargeLast_mono` and
`Prorate.chargeLast_start` (the kernel's last month, by days, from
where the month before ends: rounding is unchanged when an amount and
its denominator are scaled together, `Prorate.roundUnit_scale`) · unit
`the_charge_never_falls_and_is_continuous_over_real_calendars`
(the kernel, exhaustively: every span of 1 to 3 months from every day
of a common and a leap year, walked day by day, eight totals, both
units) · Kani's four charge harnesses (bounded: never negative, never
above the total, the total on the last day),
`prorated_through_refuses_malformed_input` · property
`prop_depreciation_is_monotone_and_bounded` (the engine)
25. **A catch-up run never carries the books past the schedule** at a
month end already posted, and posts everything due through its month
when no run comes after it (plan/served.md P3, settled in design: a
month skipped is caught up in the month the run is made); every
month's figure at once (`catch_ups`) is each suffix's catch-up. What
the books carry at each month end is read entry by entry, each run and
each reversal in its own month (`assets::depreciation_effects`), so the
kernel's law is a law of the books' month ends, not of the runs'
periods (a run reversed after its month ended is never run twice; a
re-instated run never carries the books past the schedule).
- Kani `catch_up_never_overtakes_the_schedule`,
`catch_ups_is_every_suffixs_catch_up` · bridge
`catch_up_and_true_up_agree` · unit `catch_ups_is_every_suffixs_catch_up`
· property `prop_run_amounts_are_each_months_run_amount`
· store `a_run_reversed_after_its_month_is_caught_up_in_the_reversals_month`,
`reinstating_a_run_a_later_run_caught_up_is_refused`
26. **A disposal's true-up reaches the schedule** through its day from what
the books carry, up or down, never both (`true_up` is `columns` of the
gap).
- Kani `true_up_reaches_the_target` · bridge `catch_up_and_true_up_agree`
Bounds (Kani): the split harnesses, at `i64`: up to 3 month parts, 1 to
3 months, each month 28 to 31 days with 1 to all of them in the span,
totals to 2^16 units (they also passed at 2^20, 2026-10-20), a unit of
1 or 10. The charge harnesses, at `i32` (the kernel is generic over
`Units`; `i32` so the solver can relate a product to a quotient): 1 or
2 months, any one month part and weight before it, totals to 2,048 units
(a month but the last) or 64 (the last month, whose numerator carries the
month's days), each unit in its own harness;
the malformed-input harness: totals to 2^20 either sign, months 0 to 3,
days, month lengths and k 0 to 40, a weight before of up to 4 ×
MONTH_L either sign, units -10 to 10; catch-up (and every suffix's) over
up to 3 month ends, and the true-up, at amounts to 2^40.
What the solver could not do, and what carries it instead: a harness
relating two symbolic divisions (a part against days × rate directly; a
charge never falling within a month; the charge through a month's last
day against the next month's through none of its days) ran past twenty
minutes each, at 2^20 and again at 2^16, with CaDiCaL and with Kissat,
and was stopped (2026-10-20); so did the one-division charge bound at
`i64` (a checked 64-bit product is a 128-bit multiplier to the solver).
Each such relation is nonlinear in the divisor. So Kani proves the
one-division fact at `i32` (each charge within half a unit of its exact
share: one to seven minutes a harness), the bridge ties the `i32` kernel
to the engine's `i128` one, and Lean proves every one of these relations
at any size, of the kernel's own two formulas: the model's charge is
`cum t unit V w` (the total times `w/V`, rounded half up to a multiple of
`unit`, capped at the total, and the total at `w = V`), and
`chargeMid`/`chargeLast` are `prorated_through`'s two branches written
out, proved equal to `cum` over `W` = months × MONTH_L and over `W` × the
last month's days, with the two meeting where the last month starts
(`roundUnit_scale`). The one hypothesis is the calendar's: every part
but the last ends short of the span (`e < W`), which a walk from any
in-service date keeps (B-15: a first month of at most one, then whole
months); an exhaustive test walks the kernel itself over every
1-to-3-month span of two calendar years, and the bridge and the exact
unit tests check the rest.
## What the proofs found
The engine already had 190+ tests, property tests, mutation testing,
@@ -363,8 +476,11 @@ can also prove false claims. So:
entry, a reversal on one account with several lines, three distinct
accounts, a balanced three-account chart, a close to two members, a
part-cash part-noncash investing entry, cash settling two sections at
once, a mixed entry reversed, property contributed for capital. `just
prove` fails if any cover is unsatisfiable.
once, a mixed entry reversed, property contributed for capital; a split
of three parts, a half-unit tie rounding up, a charge capped at a total
finer than its unit, a catch-up across three month ends and distinct
ones for every suffix, a true-up each way. `just prove` fails if any
cover is unsatisfiable.
## What is and isn't proved
@@ -373,10 +489,12 @@ Be precise about the claims:
- **Kani is bounded.** It proves the kernel for every input up to 4 lines
over 3 accounts and amounts to 2^40 units (the accounting equation end
to end: up to 3 accounts; the cash flow entry harnesses: up to 3 lines,
each tagged any of 4 ways). Lean proves the laws for any
size, but of a *model*. The model is a direct restatement of the kernel,
and the bridge property test ties the `Decimal` instantiation to the
`i64` one Kani checked.
each tagged any of 4 ways), and the proration by days within the bounds
above. Lean proves the laws for any size, but of a *model*. The model
is a direct restatement of the kernel (for proration, its two formulas
written out and proved equal to the model's charge), and the bridge
property tests tie the `Decimal` (and `i128`) instantiation to the
`i64` (and `i32`) one Kani checked.
- **`Decimal` itself is trusted.** `rust_decimal` is exact for these
sizes: 96-bit integers scaled by a power of ten. With lines capped at
one quadrillion and ten decimal places, every sum is exact while the
@@ -389,9 +507,10 @@ Be precise about the claims:
`tests/properties.rs` (trial balance, balance sheet, reversals,
immutability) and the golden files.
- **Out of scope:** currency (the ledger is single-currency, enforced
structurally), A/R and A/P documents, payroll, depreciation schedules
(those have their own property tests), and persistence (the journal
store's hash chain is tested in `ledger-store`).
structurally), A/R and A/P documents, payroll, the calendar walk around
the proration kernel (`calendar::month_parts`, `assets::depreciation_schedule`,
`schedules::daily_plan`: property tests, `tests/properties.rs`, P3), and
persistence (the journal store's hash chain is tested in `ledger-store`).
- **The cash flow model tags lines per entry with any function** meeting
the engine's guarantees (the cash tag follows the account; revenue and
expense lines are operating or investing; a reversal takes its