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

+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