Cosmic Bull

statements

gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements on onyx-1 testnet

pure packageprimitiveaccountinglive on onyx-1

Turn a chart of accounts into finished financial statements.

Each account's type decides its section, its sign and every total, so two readers of the same books get the same statements.

In detail curated

Classified financial statements derived from a chart of accounts, where the section, the sign and every total are functions of each account's Kind and NONE of them is supplied by the caller. SectionOf is the whole three-bucket partition -- {Asset} / {Liability,Equity} / {Revenue,Expense} -- and it is fail-closed: a Kind outside journal's five is ErrInvalidKind, never a default bucket, which matters because journal.Kind.IsDebitNormal reports the ZERO Kind as credit-normal, so a silent fallthrough would file an unclassified row under claims and produce a statement that balances and is wrong. The package deliberately ships NO Balances() method, and the reason is its own arithmetic: over a chart drawn from a journal.Book the balance-sheet identity A = L + Q + NI is algebraically identical to totalDebits == totalCredits, which the Book enforces on every Post, so such a method would be a check that CANNOT FAIL -- true even of a chart whose every account is classified backwards. Assets and Claims are both reported as figures and their equality is asserted in the tests in BOTH directions, including over a chart miscoded on purpose, which is what demonstrates the vacuity rather than reassuring about it. What ships instead is the ABNORMALITY CENSUS: Abnormal lists every row whose balance runs against its kind and AbnormalIn counts them per section, which survives a balanced book where the identity does not. The income statement is CUMULATIVE and can be nothing else -- journal's Close is a backdating seal that sweeps no nominal account into equity, so there is no retained-earnings account and no closing entry, and the field names carry the word (CumulativeRevenue/CumulativeExpenses/CumulativeNet) because a reader who takes them for a period figure misreads every statement the package produces. Claims therefore carries CumulativeNet as an explicit reconciling line. Pure: holds no state, imports no chain API, never sees a coin, and is a pure function of its rows.

Use it

A pure package has no state and no transactions: you use it by importing it.

Import it

import "gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements"

The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.

Called from real code

Not an example written for this page. These lines are quoted from statements, a deployed package in this portfolio that imports this one as stmt, out of the committed file whose sha256 onyx-1 testnet attested. They compile because they are compiled.

return stmt.MustDerive(chartOf(owner))
onyx/r/statements/statements.gno line 370, onyx-1 testnet
st := stmt.MustDerive(rows)
onyx/r/statements/statements.gno line 402, onyx-1 testnet
sec, err := stmt.SectionOf(a.Kind)
onyx/r/statements/statements.gno line 539, onyx-1 testnet

Deployments

onyx-1 testnet onyx-1

RenderedSourceCall builder

chain-attested
Import pathgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height329,571
Deploy transactione8e24ae816dfbf3ee4471708ae4363d95bf5a07b6eab961c2ed87183aa709910 look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used25,987,174
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml statements.gno
Deployed bytesstatements.gno — 18,362 bytes
sha25655f4b0b7afd06695e1d4ea844451f9ec5713d116dad547efbb420f683bd9f3e1
Realm addressnone — A /p/ package is not a realm: it holds no state, custodies no coins and is never a transaction sender. The pkgPath derivation would still produce a value; recording one would name nothing.
Used byp/periods, r/statements

Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://onyx.testnets.gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements$download&file=statements.gno' | shasum -a 256

Expected: 55f4b0b7afd06695e1d4ea844451f9ec5713d116dad547efbb420f683bd9f3e1 — 18,362 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 3 exported functions, 3 types, 4 methods.

This is a /p/ package: you import it, you do not call it in a transaction. gnoweb's $help shows only exported top-level functions, so the types and methods below do not appear there at all — which is why this reference exists.

Overview

Package statements turns a chart of accounts into classified financial statements, where the section, the sign and every total are functions of each account's kind and none of them is supplied by the caller.

A realm holds the chart — p/journal keeps one, and a realm wrapping it publishes one — and asks this package what the statements say. Nothing here reads the chain, holds state, or decides what an account is: it takes journal.Account rows and returns figures.

rows := []journal.Account{ / ascending by Code / } st, err := Derive(rows) // st.Assets, st.Claims, st.CumulativeNet, st.Abnormal

The three-bucket partition

journal partitions the five kinds in two, by normal balance. A statement partitions them in three, by where the figure is presented:

SectionAssets : {Asset} balance sheet, debit side SectionClaims : {Liability, Equity} balance sheet, credit side SectionPerformance : {Revenue, Expense} income statement

SectionOf is the whole mapping and it is fail-closed: a Kind outside 1..5 is ErrInvalidKind, never a bucket. The zero Kind matters here because journal.Kind.IsDebitNormal reports it as credit-normal, so a silent fallthrough would not drop the row — it would file it under claims and produce a statement that balances and is wrong.

There is no Balances method, and the reason is the product's own arithmetic

Write A, L, Q, R and X for the natural-sign totals of the five kinds, each the sum of journal.Account.Balance over its kind, and NI = R - X. For any single account, Balance() is RawDelta() for a debit-normal kind and its negation otherwise, so

sum over every account of RawDelta = A - L - Q - R + X

and in a balanced book that sum is totalDebits - totalCredits = 0. Hence

A = L + Q + NI

which is the balance sheet identity. The consequence is uncomfortable and is stated here rather than discovered by a reader: **over a chart drawn from a journal.Book the identity is a constant true**, algebraically identical to `totalDebits == totalCredits`, which the Book enforces on every Post. It is true of a chart whose every account is classified backwards. A `Balances()` returning that comparison would be a check that cannot fail, which this project has shipped before and does not ship again — see DISCOVERY.md § 4.

The identity is NOT vacuous over this package's own input domain, because rows may come from anywhere and a hand-built set need not sum to zero. That scope is the whole content of the claim, and dropping a scope is the documentation defect this project has recorded most often. So:

What ships instead: the abnormality census

journal.Account.Balance is natural-sign and journal's own doc says a negative result is "a real signal (a contra position)". What did not exist is the roll-up: finding an abnormal account meant already knowing to look at it. [Statement.Abnormal] lists every row whose balance runs against its kind, and Statement.AbnormalIn counts them per section. It survives a balanced book, which the identity does not.

It is the only partial answer in this portfolio to journal's obligation 7 — "a Kind set wrongly at Open time is undetectable here and undetectable anywhere else in this package" — and it is sound but incomplete, with the incompleteness exactly characterisable:

The income statement is cumulative and can be nothing else

journal.Book.Close is a backdating seal: it refuses entries dated at or before a cutoff and does not sweep nominal accounts into equity. There is no retained-earnings account and no closing entry, so revenue and expense accumulate from the book's first posting forever. The field names carry the word — [Statement.CumulativeRevenue], [Statement.CumulativeExpenses], [Statement.CumulativeNet] — because a reader who takes them for a period figure misreads every statement this package produces.

[Statement.Claims] therefore includes CumulativeNet as an explicit reconciling line. A reader who checks Liabilities + Equity against Assets and finds it short by exactly CumulativeNet is not looking at a bug. A period income statement needs entry-level data and is out of scope; so is a cash-flow statement, which needs a cash-vs-accrual classification journal.Posting does not carry.

What this package does not claim

p/journal: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal

Imports

Constants and variables

MaxRows bounds the chart Derive will classify, and so bounds its only loop. It is journal's own account cap, because that is the largest chart this ecosystem's bookkeeping primitive can produce, and a row set larger than that did not come from a chart.

What the bound costs, stated rather than waved at: Derive is one pass with a checked add and a comparison per row, so a chart at the limit is 10,000 iterations in one call. A realm that classifies several charts in one transaction, or renders them, multiplies that. The realm wrapping this package will have a much smaller cap of its own — r/books allows 128 — and should: this bound is the ceiling a caller may ask for, not a size worth reaching.

const MaxRows = journal.MaxAccountsLimit

Errors returned by this package.

var (
	// ErrInvalidKind is returned for a row whose Kind is outside journal's
	// five. It is fail-closed by choice: the alternative is a row filed
	// under claims by [journal.Kind.IsDebitNormal]'s treatment of the zero
	// value, which yields a statement that balances and misstates.
	ErrInvalidKind = errors.New("statements: unknown account kind")

	// ErrRowsNotOrdered is returned when rows are not in strictly ascending
	// Code order. The precondition does three jobs at once and that is why
	// it is a precondition rather than an internal sort: it rules out a
	// duplicate code silently double-counting a balance, it keeps
	// [Statement.Abnormal] in a deterministic order a Render can rely on,
	// and it costs one string comparison per row instead of an allocation
	// and a sort.
	//
	// A row with an empty Code fails this check, since no string sorts
	// before "". journal refuses an empty code at Open, so a Book-derived
	// chart cannot contain one.
	//
	// Every producer in this portfolio already satisfies it:
	// [journal.Book.IterateAccounts] walks an avl tree keyed by code, and
	// r/books' Codes pages that same order.
	ErrRowsNotOrdered = errors.New("statements: rows must be in strictly ascending code order")

	// ErrTooManyRows is returned when len(rows) exceeds MaxRows.
	ErrTooManyRows = errors.New("statements: row count exceeds MaxRows")

	// ErrNegativeTotals is returned for a row whose Debits or Credits column
	// is negative. Such a row cannot have come from a [journal.Book]:
	// postings carry positive amounts only and an account's two columns are
	// monotone non-decreasing.
	//
	// It is checked for a reason beyond tidiness, and the reason is what
	// makes this package's overflow story complete.
	// [journal.Account.Balance] subtracts one column from the other with no
	// check of its own, so a row carrying Debits near max and a NEGATIVE
	// Credits yields a WRAPPED balance before any arithmetic here runs — and
	// no amount of checked adding downstream can recover a figure that was
	// already wrong on arrival. With both columns non-negative that
	// subtraction lands in [-maxInt64, maxInt64] and cannot wrap, which is
	// the precondition the checked adds in [Derive] are sufficient under.
	ErrNegativeTotals = errors.New("statements: account column totals must not be negative")

	// ErrOverflow is returned when a section total, the cumulative net, or
	// the claims subtotal would wrap int64. Nothing is reported partially:
	// an overflowing statement is an error, not a wrapped number, because a
	// wrapped total is a figure a reader cannot tell from a real one.
	ErrOverflow = errors.New("statements: int64 overflow")

	// ErrInvalidSection is returned by [Statement.SectionTotal] for a
	// section outside the declared three.
	ErrInvalidSection = errors.New("statements: unknown section")
)

The three statement sections.

const (
	SectionAssets      Section = 1 // balance sheet, debit side
	SectionClaims      Section = 2 // balance sheet, credit side
	SectionPerformance Section = 3 // income statement
)

Types

type Abnormal

type Abnormal struct {
	Code    string
	Name    string
	Kind    journal.Kind
	Balance int64
}

Abnormal is one account whose balance runs against its kind's normal direction. Balance is in the account's normal direction and is therefore always negative here.

Derive is its only producer in this package.

type Section

type Section uint8

Section is where an account's figure is presented. The zero value is deliberately invalid, so a forgotten field is an error rather than assets.

The constants carry a Section prefix on purpose: Statement has fields named Assets and Claims, and bare constants of the same words would read as the same thing in two namespaces.

Section.String

func (s Section) String() string

String returns the section's name, or "invalid" for the zero value and any unrecognised code.

Section.Valid

func (s Section) Valid() bool

Valid reports whether s is one of the three sections.

type Statement

type Statement struct {
	// Rows is how many accounts were classified. Zero is a legitimate
	// statement of nothing, not an error.
	Rows int

	// Assets is the balance-sheet debit side.
	Assets int64

	// Liabilities and Equity are the two credit-side components, reported
	// separately because a reader needs them apart and Claims does not
	// recover them.
	Liabilities int64
	Equity      int64

	// CumulativeRevenue and CumulativeExpenses accumulate from the book's
	// first posting, not over a period. See the package doc.
	CumulativeRevenue  int64
	CumulativeExpenses int64

	// CumulativeNet is CumulativeRevenue - CumulativeExpenses. It appears on
	// the balance sheet as a reconciling line because journal closes no
	// nominal account into equity.
	CumulativeNet int64

	// Claims is Liabilities + Equity + CumulativeNet: the credit side of the
	// balance sheet as presented.
	Claims int64

	// Abnormal lists every row whose balance runs against its kind, in the
	// row order Derive received, which ErrRowsNotOrdered forces to be
	// ascending by code. It is never nil for a Statement from Derive.
	Abnormal []Abnormal
}

Statement is the classified figures for one chart at one instant.

Every int64 field is a natural-sign total: a normal chart has them all non-negative. They are values, not pointers, so a holder cannot reach back into anything — except through Abnormal, which is a slice and is freshly allocated by Derive on every call, so the caller owns it.

Assets and Claims are both reported and their equality is deliberately not offered as a method; see the package doc on why that comparison cannot fail over a Book-derived chart.

Exported fieldTypeDoc
RowsintRows is how many accounts were classified. Zero is a legitimate statement of nothing, not an error.
Assetsint64Assets is the balance-sheet debit side.
Liabilitiesint64Liabilities and Equity are the two credit-side components, reported separately because a reader needs them apart and Claims does not recover them.
Equityint64
CumulativeRevenueint64CumulativeRevenue and CumulativeExpenses accumulate from the book's first posting, not over a period. See the package doc.
CumulativeExpensesint64
CumulativeNetint64CumulativeNet is CumulativeRevenue - CumulativeExpenses. It appears on the balance sheet as a reconciling line because journal closes no nominal account into equity.
Claimsint64Claims is Liabilities + Equity + CumulativeNet: the credit side of the balance sheet as presented.
Abnormal[]AbnormalAbnormal lists every row whose balance runs against its kind, in the row order Derive received, which ErrRowsNotOrdered forces to be ascending by code. It is never nil for a Statement from Derive.

Statement.AbnormalIn

func (s Statement) AbnormalIn(sec Section) int

AbnormalIn counts the abnormal rows presented in sec.

It recomputes each row's section from its Kind through SectionOf rather than storing one, so there is one definition of the partition and no second field that could disagree with it. A Statement built by hand can hold a row with an invalid Kind, and such a row is counted in no section; Derive cannot produce one.

Statement.SectionTotal

func (s Statement) SectionTotal(sec Section) (int64, error)

SectionTotal returns the figure presented for sec: Assets, Claims, or CumulativeNet. A section outside the three is ErrInvalidSection.

Note what the performance total is: the income statement's bottom line, not the sum of its two components. Revenue and expenses are read from their own fields.

Functions

Derive

func Derive(rows []journal.Account) (Statement, error)

Derive classifies rows and returns the statement figures.

rows must be in strictly ascending Code order (ErrRowsNotOrdered) and no longer than MaxRows. Every Kind must be one of journal's five (ErrInvalidKind) and neither column may be negative (ErrNegativeTotals, which is what makes the checked adds here sufficient). Every accumulation is checked (ErrOverflow).

It is a pure function of rows: the same rows always give the same Statement, and nothing about the caller, the chain or the time enters it. On any error the returned Statement is the zero value, so a caller that ignores the error reads zeros rather than a half-summed chart.

MustDerive

func MustDerive(rows []journal.Account) Statement

MustDerive is Derive and panics instead of returning an error. It is for a realm's mutating path, where an error return does not revert the transaction and only a panic does.

SectionOf

func SectionOf(k journal.Kind) (Section, error)

SectionOf maps an account kind to the section it presents in. An invalid kind is ErrInvalidKind; there is no default bucket.

This is the only place in the package that knows the partition, and it is exported so a consumer grouping rows for presentation uses the same mapping the totals were built from rather than a second copy of it.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importserrors, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal
First-party dependenciesgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source fileonyx/p/statements/statements.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical