Skip to content

SPEC 011 Reporting Framework

Rafael Queiroz edited this page Sep 15, 2026 · 4 revisions

SPEC-011 — Reporting Framework

Status Draft
PRD source F5.0 (FR-5.25–5.35)
Depends on SPEC-009 (snapshots), SPEC-010 (wallets)
Required by SPEC-012, SPEC-013, SPEC-014, SPEC-015, SPEC-017
Milestone M5

Description

The shared skeleton every report is built on: the period, scope and grouping controls, and the guarantees that hold across all four reports.

Scope and grouping are independent and answer different questions. Scope narrows what is included — the whole portfolio, or one wallet. Grouping decides how the included data is broken apart — by asset class, wallet, individual asset, sector or institution. A user can scope to one wallet and still group its contents by asset class.

Building this once, rather than four times, is what makes the controls behave identically everywhere — a user who learns them on one report can use them on all of them.

Business rules

ID Rule
BR-011-01 Every report accepts a period: YTD, 12m, 24m, all time, or a custom range.
BR-011-02 Every report accepts a scope: whole portfolio, or a single wallet.
BR-011-03 Every report accepts a grouping dimension and must present its figures broken down by it: asset class, wallet, individual asset, sector, or institution.
BR-011-04 Asset class is the default grouping at portfolio scope; individual asset is the default within a single-wallet scope. Both are user-configurable (SPEC-002).
BR-011-05 The three controls are independent — any combination is valid.
BR-011-06 Grouping behaves identically across all four reports: same control, same options, same semantics.
BR-011-07 Groups are expandable: a group row drills down to its constituent assets without leaving the report.
BR-011-08 Switching the grouping must never change any scope-level total. Every grouped view sums to the same figure.
BR-011-09 Grouping by wallet at portfolio scope includes Unassigned as a visible group, so allocated and unallocated holdings always sum to the portfolio.
BR-011-10 Where a grouping does not apply to part of the scope — sector is undefined for fixed income — those holdings appear in an explicit "Not classified" group. They are never dropped from the total.
BR-011-11 Period, scope and grouping are reflected in the URL so a view can be bookmarked and returned to.
BR-011-12 Any grouped view is exportable to CSV with the grouping preserved, with formula-injection neutralisation (SPEC-003, BR-003-13).
BR-011-13 Reports read from DailyValuationSnapshot rather than recomputing from the ledger per request (SPEC-009, BR-009-16). One bounded exception: an earnings read whose window contains a leilão de frações provento replays only the assets that paid one in that window, up to its end, to learn the quantity held on each payment date — which wallet attribution needs (SPEC-014 BR-014-12) and no stored figure carries. It never replays an asset without such a payment. (Amended 2026-09-15, #113 — see DL-011-07.)
BR-011-14 Any report renders within 3s (p95) for 100 assets and 10,000 transactions over a 5-year period.
BR-011-15 Where a report includes estimated values (accrued fixed income), the estimate is marked (SPEC-009, BR-009-11).
BR-011-16 A scope with no data renders an explanatory empty state, never an error or a misleading zero.

Acceptance Criteria

  • All four reports expose identical period, scope and grouping controls.
  • Every period option produces correct date boundaries, including a custom range.
  • Scoping to a wallet excludes all other holdings from every figure on every report.
  • All five grouping dimensions work on all four reports.
  • Default grouping is asset class at portfolio scope and individual asset at wallet scope.
  • Switching grouping from asset class to wallet and back leaves every total unchanged — asserted automatically on all four reports.
  • Grouping by wallet at portfolio scope shows Unassigned, and the groups sum to the portfolio total.
  • Grouping by sector with fixed income present shows a "Not classified" group, and totals still reconcile.
  • Expanding a group reveals its constituent assets in place.
  • Copying the URL and opening it in a new session reproduces the same period, scope and grouping.
  • CSV export preserves the grouping and neutralises formula-injection cells.
  • Reports read from snapshots — verified by asserting no full ledger recomputation occurs on a report request.
  • p95 render is under 3s at the stated scale.
  • An empty wallet renders an explanatory empty state.
  • Estimated values are visually distinguished on every report that shows them.

Out of Scope

  • The content of any individual report (SPEC-012 through SPEC-015).
  • Return, income and composition calculations themselves.
  • Custom user-defined groupings beyond the five dimensions.
  • Saved report configurations, scheduled reports or emailed reports.
  • PDF export — CSV only in v1.
  • Cross-period comparison views (e.g. this year vs last year side by side) beyond what individual reports specify.
  • Charting library selection and visual design.
  • Drill-through from a report to the underlying transaction list.

Decision Logs

# Decision Alternatives considered Rationale
DL-011-01 Scope and grouping are independent controls. A single combined "view by" control Combining them cannot express "this wallet, broken down by asset class" — a natural and common question. Independence costs one more control and answers a much larger set of questions.
DL-011-02 A shared framework rather than four report implementations. Build each report's controls separately Separate implementations drift: the grouping options diverge, the period boundaries stop matching, and totals between reports stop reconciling. Building it once is what makes BR-011-08 enforceable at all.
DL-011-03 Totals must be invariant across groupings, asserted automatically. Trust that aggregation is correct This is a cheap, powerful correctness check: nearly every aggregation bug — double-counting a split allocation, dropping an unclassified holding — shows up as a total that changes when the grouping changes. Turning it into an automated assertion catches a whole bug class.
DL-011-04 "Not classified" group rather than dropping unclassifiable holdings. Exclude holdings the grouping does not apply to Silently dropping fixed income from a sector view makes the total disagree with every other view, and the user cannot see why. An explicit group keeps the arithmetic honest and visible.
DL-011-05 Unassigned is always a visible group when grouping by wallet. Hide it; force full allocation Hiding it makes wallet groups fail to sum to the portfolio — the "every number is explainable" principle forbids it (consistent with SPEC-010 DL-010-07).
DL-011-06 State in the URL. Client-side state only Bookmarking a view and returning to it is a basic expectation, and URL state makes support conversations concrete — a user can send the exact view they are asking about.
DL-011-07 Reports read from snapshots, not the ledger. Compute on demand for guaranteed freshness On-demand computation over five years of daily data cannot meet the 3s budget. Snapshots stay correct because SPEC-009 rebuilds them forward whenever a transaction changes. Exception (2026-09-15, #113): a leilão de frações row states the fraction sold, not the shares it was paid on, so BR-014-12's wallet split needs the position held on its payment date. The report replays only the assets that paid one (a handful of rows per account), up to the period end. Rejected: storing that quantity on the row, which is derived data on the source-of-truth ledger and goes stale whenever earlier history is imported or edited unless every write path recomputes it; and a per-asset snapshot, whose rows multiply by every asset over every day. The blocking structural check allows this one replay by name.

Clone this wiki locally