reportkit: make the monthly revenue report trustworthy and linear [J-GAUNTLET-529843] - #14
Open
hrabbani wants to merge 2 commits into
Open
reportkit: make the monthly revenue report trustworthy and linear [J-GAUNTLET-529843]#14hrabbani wants to merge 2 commits into
hrabbani wants to merge 2 commits into
Conversation
added 2 commits
August 28, 2026 04:40
Finance could not trust the monthly revenue report and it was slow on the full export. Three independent defects were responsible. Parsing: parse_rows split every line on "," so a quoted description containing a comma shifted every later column, and the resulting short row was then silently dropped -- quietly understating revenue. Field splitting now goes through the stdlib csv reader. A wrong field count raises instead of dropping money, a headerless export no longer loses its first row, and blank lines are ignored. Money: totals accumulated into a binary float, so 60k realistic amounts summed to 501580.7100000015 instead of 501580.71. Amounts are now parsed into decimal.Decimal from the original strings and accumulated exactly. NaN/Infinity, non-numeric amounts and malformed dates are rejected instead of poisoning or bucketing money silently. Rounding policy: previously unrecorded, and the README forbade guessing it. Decided with finance and accounting and now recorded in aggregate.py and README.md -- exact internally, rounded only at presentation to 2dp with ROUND_HALF_EVEN via the new format_amount(). Aggregation never rounds. Grouping: monthly_totals rescanned every row once per month (O(rows x months)) and months() did a linear list scan per row. Both are single-pass now. At 60k rows the old code went 0.074s -> 0.316s as distinct months went 12 -> 60; the new code stays flat at 0.055s. Empty months: monthly_average divided by zero for an absent month. It now returns None, which says "no data" instead of claiming an average of zero. [J-GAUNTLET-529843]
Two defects found while adversarially probing the first commit.
A UTF-8 BOM, which spreadsheet exports routinely carry, stopped the header
record from matching, so the header was parsed as a data row and then blew
up in month_of. parse_rows now strips a leading BOM.
Decimal() on its own is more permissive than a money column should be: it
accepts NaN/Infinity, underscore grouping ("1_000" -> 1000) and non-ASCII
digits ("12" fullwidth -> 12), silently turning corrupt input into a
plausible-looking number. amount_of now validates against an explicit ASCII
decimal grammar first.
Both behaviours are covered by new tests, along with CRLF line endings and
newlines inside quoted descriptions.
[J-GAUNTLET-529843]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes HUM-16 — "Monthly revenue report cannot be trusted (full)".
Nobody had written down what was actually wrong. Five independent defects were, and each one is reproduced with evidence below. Scope is limited to
experiments/J/fixture; no new dependencies (standard library only).1. Quoted fields shifted every later column, then the row was silently dropped
parse_rowssplit each line on every",", so a description containing a comma pushed the amount out of its column. The short row then hitif len(parts) < len(HEADER): continueand was silently discarded — money vanished from the report with no error.Field splitting now goes through the stdlib
csvreader (which also handles doubled quotes and newlines inside quoted fields correctly — a hand-rolled quote scanner does not). A record whose field count is wrong now raises instead of silently dropping revenue.2. Money was accumulated in binary float
Totals were summed into a
float. Real money does not survive that:Amounts are now parsed into
decimal.Decimalfrom the original strings and accumulated exactly.3. Grouping was quadratic
monthly_totalslooped over every month and rescanned every row inside that loop (O(rows × months)), andmonths()did a linearinscan over a list for each row. Both are single pass now. Measured on 60k rows — note the old code degrades with month count while the new code stays flat:This is guarded by a deterministic, non-timing test:
_CountingRowscounts iterations and asserts exactly one pass, so any regression to a per-group rescan fails the suite rather than flaking.4. Empty or missing months blew up
monthly_averagedivided bylen(amounts)unconditionally and raisedZeroDivisionErrorfor a month with no rows. It now returnsNone, which reports "no data" honestly — returning0would claim a real average of zero.monthly_totals([])andmonths([])are also exercised.5. Malformed dates and amounts produced plausible-looking nonsense
row.date[:7]was unchecked, so an empty or malformed date quietly created a junk bucket ("","2026") that still carried real money. Separately,Decimal()is more permissive than a money column should be. Both are now validated:All now raise
ValueErrornaming the offending row.Also fixed while probing: a UTF-8 BOM — which spreadsheet exports routinely carry — stopped the header from matching, so the header was parsed as a data row and then crashed on its date. And a headerless export previously lost its first data row, because the old code always dropped line 1.
Rounding policy — escalated, not guessed
The ticket and
README.mdboth said the rounding/presentation policy had never been decided and must not be guessed. I verified that independently before asking:experiments/Jhas exactly one commit (f9cbe8b), and a repo-wide search forround|quantize|ROUND_HALF|banker|two decimal|cents|currencyreturns only unrelated hits ("round-trip", experiment "rounds"). No commit message mentions it.Finance and accounting have now agreed, and the decision is recorded in the code (
aggregate.py) and inREADME.mdso nobody has to ask again:Decimal; it never touches binary float.ROUND_HALF_EVEN).monthly_totals()/monthly_average()therefore return exact, unrounded values;format_amount()is the single place rounding is applied.Aggregation deliberately never quantizes: rounding intermediates would reintroduce the very drift the policy exists to prevent.
Validation
python3 -m pytest experiments/J/fixture/tests -q→ 28 passed (was 2 passed, 1 failed).Reviewer notes
The one judgement call worth your attention: parse errors now raise instead of being silently skipped. A silently dropped row understates revenue, which is the "cannot be trusted" complaint itself, so I chose loud failure over quiet loss. The ticket's "must not blow up" requirement is about empty/missing months, which is handled separately by returning
None. Happy to switch to a collected-warnings model if you'd rather the report always render.Devin Review