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))st := stmt.MustDerive(rows)sec, err := stmt.SectionOf(a.Kind)Deployments
onyx-1 testnet onyx-1
chain-attested| Import path | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements |
|---|---|
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Deployed at height | 329,571 |
| Deploy transaction | e8e24ae816dfbf3ee4471708ae4363d95bf5a07b6eab961c2ed87183aa709910 look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 25,987,174 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml statements.gno |
| Deployed bytes | statements.gno — 18,362 bytes |
| sha256 | 55f4b0b7afd06695e1d4ea844451f9ec5713d116dad547efbb420f683bd9f3e1 |
| Realm address | none — 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 by | p/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 256Expected: 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:
- [Statement.Assets] and [Statement.Claims] are both reported, as figures.
- Their equality is asserted in the tests, in both directions: held over a Book-derived chart, and shown to survive a chart miscoded on purpose, which is what makes it vacuous there rather than reassuring.
- Nothing in the API offers that comparison as a verdict.
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:
- It CATCHES a misclassification that crosses the debit/credit-normal boundary and whose account has a non-zero balance. A payables account coded as an asset accumulates credits and reads negative.
- It CANNOT CATCH a misclassification that stays on the same side: Liability <-> Equity <-> Revenue, or Asset <-> Expense. Both subtotals move, no balance goes negative, and nothing anywhere detects it. Obligation 7 is narrowed, not closed.
- It has a FALSE-POSITIVE MODE that is not a defect: a genuine contra account — accumulated depreciation under assets, a sales return under revenue — is legitimately abnormal-signed. The census is a finding to read, never a verdict.
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
- **Nothing about money.** A statement is a report about a book, and a book is a set of claims by its owner with no custody behind it. This package never sees a coin. A realm publishing its output must say so loudly: a published statement adds apparent authority to a self-report, and that is the one way this product can mislead someone.
- **No audit.** Every figure here is derived from rows the caller supplied. Deriving them faithfully is the guarantee; that they describe anything real is not.
- **Two statements and a census**, not a statement set. No statement of changes in equity, no notes, no comparatives.
p/journal: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal
Imports
errorsgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal
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 field | Type | Doc |
|---|---|---|
Rows | int | Rows is how many accounts were classified. Zero is a legitimate statement of nothing, not an error. |
Assets | int64 | Assets is the balance-sheet debit side. |
Liabilities | int64 | Liabilities and Equity are the two credit-side components, reported separately because a reader needs them apart and Claims does not recover them. |
Equity | int64 | |
CumulativeRevenue | int64 | CumulativeRevenue and CumulativeExpenses accumulate from the book's first posting, not over a period. See the package doc. |
CumulativeExpenses | int64 | |
CumulativeNet | int64 | CumulativeNet is CumulativeRevenue - CumulativeExpenses. It appears on the balance sheet as a reconciling line because journal closes no nominal account into equity. |
Claims | int64 | Claims is Liabilities + Equity + CumulativeNet: the credit side of the balance sheet as presented. |
Abnormal | []Abnormal | 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. |
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| Imports | errors, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal |
|---|---|
| First-party dependencies | gno.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- NO CONSUMER IS ON CHAIN, SO Derive OVER REAL ROWS IS NOT LIVE-PROBED -- BUT THE BOUNDARY IS NARROWER THAN THE FIRST DRAFT OF THIS BULLET CLAIMED, AND THE CORRECTION WAS MEASURED, NOT REASONED. onyx/r/statements is written, audited and sabotage-covered against these exact bytes but is NOT deployed. A bare /p/ declares no crossing functions, so there is no MsgCall path into it, and the imported `journal` identifier is NOT bound in a vm/qeval expression context (`MustDerive([]journal.Account{})` -> `name journal not declared`, measured twice), so Derive cannot be handed real rows from outside a consuming realm. What that does NOT block is anything reachable through the package's OWN types: Statement and Abnormal are in-package, so their composite literals ARE constructible in an expression, and a field whose type is a FOREIGN named type takes an untyped constant -- Abnormal{Code: "1000", Kind: 1, Balance: -5} type-checks on chain. The first draft of this bullet therefore listed 'the abnormality census over a miscoded chart' and 'every non-zero total' among the unreached, AND BOTH WERE FALSE WHEN WRITTEN. They were falsified by issuing the probes, not by re-reading the prose -- which is exactly how the vm/qeval binding limit itself was found, one notch earlier. PROVEN LIVE, every read chain_id_bound: true: MaxRows == 10000, which is journal.MaxAccountsLimit and so proves the cross-package import RESOLVED on chain rather than merely that these bytes are stored; SectionOf(2) == SectionClaims with a nil error; SectionOf(9) == (0, 'statements: unknown account kind'), the fail-closed branch, with error text specific to the audited source; SectionPerformance.String() == 'performance' and Section(42).String() == 'invalid', the switch default; MustDerive(nil) runs Derive and MustDerive end-to-end over the empty chart; Statement{Assets: 7, Claims: 7, CumulativeNet: -3}.SectionTotal(SectionPerformance) == (-3, nil), a NON-ZERO total through the branch that distinguishes the bottom line from the sum of its components; Statement{Abnormal: []Abnormal{{Code: "1000", Kind: 1, Balance: -5}}}.AbnormalIn(SectionAssets) == 1, which routes a journal.Kind through SectionOf and counts the match; and MustDerive(nil).SectionTotal(0) == (0, 'statements: unknown section'), the second fail-closed guard. Note the type is reported as the fully-qualified gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements.Section, not a bare Section. WHAT REMAINS GENUINELY UNREACHED, and this distinction is the whole correction: the census READER is probed and the census PRODUCER is not. Nothing on chain has run the branch inside Derive that DECIDES a row is abnormal -- AbnormalIn was handed its slice by the expression. Likewise unreached: every checked-add overflow path, ErrRowsNotOrdered, ErrNegativeTotals, ErrTooManyRows, and every figure Derive computes rather than a caller supplies. Those rest on 27 local unit tests (27 declared / 27 ran / 27 PASS / 0 FAIL, name sets identical, no subtests) and 58 sabotage cases (0 uncaught, 0 did not apply, 0 timed out, FULLY COVERED) under the pinned onyx toolchain, with the vm/qfile byte-match transferring that local evidence to this chain. DO NOT READ THIS BULLET AS PERMANENT: the equivalent sentence on p/journal was written as structural ('no probe can change that') and went FALSE in one day when its consumer landed, and this bullet's own first draft went false in under an hour. The contingent half here is 'nothing consumes it yet'; the structural half is the foreign-package binding limit, which a consumer deploy does not remove but does route around.
- GATE B WAS RUN AFTER THE BROADCAST, NOT IMMEDIATELY BEFORE IT, AND THAT IS A DEVIATION FROM CLAUDE.md 7a RATHER THAN A RESTATEMENT OF IT. The rule puts Gate B at TESTNET VALIDATION immediately before deploy precisely because the working tree may drift between Gate A and the broadcast and because first-party dependencies may land in between. Neither happened -- Gate A ran minutes earlier on a tree verified identical to HEAD, every action between the two was a read, and Gate A used --include-tests, a strict SUPERSET of the chain-executed closure -- so the substance Gate B checks was established before the broadcast. What was actually skipped is the separately-timed run whose JSON record the deployment record is supposed to carry. Both gates now read EQUIVALENT with the SAME tested-closure digest 66d146fe20d9f0eb112580d080319e69bb6235976e863be5218c2c7b77b09b20 appearing as the chain-closure digest too, over 2 compared dependencies. The ordering defect produced no wrong verdict here; recording it as 'Gate B: EQUIVALENT' without the timing would have been the citation that cannot be reproduced.
- THE COMMITTED GATE A RECORD READ DIVERGENT UNTIL THIS DEPLOY, FOR A NAMED AND CONTROLLED REASON. The superseded run recorded equivalent: false with exactly two failures -- 'gno.land/p/nt/avl/v0 is in the tested closure but NOT in the chain closure' and 'journal is a tested dependency but absent from the target chain' -- both of which were the SAME fact: journal was not yet deployed, so the chain-side closure walk could not reach avl through it. The flip to EQUIVALENT is a single-variable change and is mechanically demonstrable: the tested-closure digest is BYTE-IDENTICAL across the two runs, and checker_sha256, include_tests, gnoroot, gnohome, rpc and stdlib_residual all read SAME, so only the chain side moved. That is the dependency ordering p/journal -> p/statements being proven by measurement rather than asserted.
- simulate=true ON onyx-1 DOES NOT TYPE-CHECK, because of the chain's inert code-submission policy, so the mandatory dry run before this broadcast was a gas/fee/authorization probe only -- it returned gas_used 25,987,184 against the broadcast's 25,987,174, a 10-unit difference. UNLIKE p/journal's deploy, the correctness substitute WAS run before the broadcast here: `gno lint .` against the onyx release, exit 0 with zero output, on bytes proven identical to HEAD. That is the gate p/journal's record had to admit it cleared only afterwards.
- THE STORAGE DEPOSIT IS BALANCE-DERIVED, NOT CHAIN-ATTESTED. onyx-1 emits no StorageDepositEvent, so storage_bytes and storage_deposit are null here as on every onyx-1 entry. A pre-broadcast and a post-broadcast gno_account read bracket the cost -- 14,377,572 then 12,868,410 ugnot, both chain_id_bound: true, with the sequence moving 48 -> 49 and so by exactly ONE step, which is what makes the delta attributable to this single transaction. Delta 1,509,162 ugnot less the exactly-reproduced fee of 77,962 leaves a 1,431,200 ugnot residual. That residual is an INFERENCE FROM TWO BALANCES, not a figure the chain attested.
- THE BALANCE-SHEET IDENTITY THIS PACKAGE REPORTS IS A CONSTANT TRUE OVER ITS PRIMARY INPUT, and the package says so in its own doc rather than leaving a reader to find out. Over a chart drawn from a journal.Book, Assets == Claims is algebraically identical to totalDebits == totalCredits, which the Book already enforces on every Post, so it holds even of a chart whose every account is classified backwards. No Balances() verdict is offered anywhere in the API -- only the two figures. The identity is NOT vacuous over the package's own wider input domain, since rows may be hand-built and need not sum to zero, and THAT SCOPE IS THE WHOLE CONTENT OF THE CLAIM: dropping it is the documentation defect this project has recorded most often.
- THE ABNORMALITY CENSUS IS SOUND BUT INCOMPLETE, AND THE INCOMPLETENESS IS EXACTLY CHARACTERISABLE. It CATCHES a misclassification that crosses the debit/credit-normal boundary on an account with a non-zero balance. It CANNOT CATCH one that stays on the same side -- Liability <-> Equity <-> Revenue, or Asset <-> Expense -- because both subtotals move, no balance goes negative, and nothing anywhere detects it; journal's consumer-contract obligation 7 is NARROWED, NOT CLOSED. It also has a FALSE-POSITIVE MODE that is not a defect: a genuine contra account, such as accumulated depreciation under assets or a sales return under revenue, is legitimately abnormal-signed. The census is a finding to read, never a verdict.
- A STATEMENT IS A REPORT ABOUT A BOOK, AND A BOOK IS A SET OF CLAIMS BY ITS OWNER WITH NO CUSTODY BEHIND IT. This package never sees a coin and performs no audit: every figure is derived from rows the caller supplied, so deriving them faithfully is the guarantee and that they describe anything real is not. A realm publishing this output must say so loudly, because a published statement ADDS APPARENT AUTHORITY TO A SELF-REPORT, and that is the one way this product can mislead someone.
- THE CLOSURE VERDICT IS THIN AND THE TOOLCHAIN IS SEALED, NOT ATTESTED. Both gates are EQUIVALENT over 2 compared chain dependencies -- journal (first-party) and gno.land/p/nt/avl/v0 -- which is more than p/journal's one and is still two packages. The residual differs between the gates while the verdict does not: 17 stdlib packages under --include-tests against 6 without. Stdlibs execute from the node binary and are not comparable, so equivalence for them rests on the sealed onyx GNOROOT matching the node's build, which the node does not expose. Per CLAUDE.md 7a a pin proves only that nothing has changed since it was written, never that it was right when written.
- TWO STATEMENTS AND A CENSUS, NOT A STATEMENT SET. No statement of changes in equity, no notes, no comparatives. A PERIOD income statement is out of scope because it needs entry-level data this package never receives, and a cash-flow statement is out of scope because it needs a cash-vs-accrual classification journal.Posting does not carry. MaxRows is journal's own 10,000-account cap and is a CEILING A CALLER MAY ASK FOR, NOT A SIZE WORTH REACHING: Derive is one pass with a checked add and a comparison per row, so a chart at the limit is 10,000 iterations in a single call, and the wrapping realm should and does cap far lower (r/books allows 128).
- ErrRowsNotOrdered IS A PRECONDITION, NOT AN INTERNAL SORT, and that is load-bearing in three ways at once: it rules out a duplicate code silently double-counting a balance, it keeps 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 it, since no string sorts before the empty string -- journal refuses an empty code at Open, so a Book-derived chart cannot contain one. ErrNegativeTotals is likewise not tidiness: journal.Account.Balance subtracts one column from the other with no check of its own, so a row with Debits near max and a NEGATIVE Credits yields a WRAPPED balance before any arithmetic here runs, and no amount of checked adding downstream recovers a figure that was already wrong on arrival. Refusing negative columns is the precondition under which the checked adds in Derive are sufficient.
- AL29b, 2026-10-15: Derive(nil) IS NOW PROBED DIRECTLY, AND LIMITATION 0's QUALIFIER IS CONFIRMED EXACT RATHER THAN CORRECTED. New probe (28 in onyx/AL29-behavioral-probes.json), predicted before the read, stamped chain_id_bound: true, agreeing: func() (int, int64, int64, bool, bool) { st, err := Derive(nil); return st.Rows, st.Assets, st.Claims, st.Abnormal != nil, err == nil }() -> (0 int) (0 int64) (0 int64) (true bool) (true bool). IT IS A WEAK PROBE AND IS LABELLED SO: an all-zero tuple whose single non-vacuous bit is st.Abnormal != nil, true only because Derive explicitly initialises []Abnormal{} -- a zero Statement{} returns false there. Weaker than p/cashflow's three verbatim sentinels. LIMITATION 0 WAS AND REMAINS CORRECT, and that is the finding. It says 'Derive OVER REAL ROWS is not live-probed' and lists 'MustDerive(nil) runs Derive and MustDerive end-to-end over the empty chart' under 'PROVEN LIVE' -- consistent and precise, because MustDerive(nil) passes an EMPTY chart and the unbound-`journal` limit is what keeps real rows out. Both catalog claims were re-issued today (probes 29-30) and BOTH REPRODUCE EXACTLY, including MustDerive(nil).SectionTotal(0) -> (0 int64) and the *errors.errorString carrying 'statements: unknown section'. WHAT WAS FALSE WAS AN EXTERNAL RECORD: [an unpublished record]'s first AL29 amendment listed 'statements.Derive(nil)' in 'the un-attempted set', while this entry had recorded the attempt since commit bc01165. MustDerive is a one-line wrapper calling Derive and panicking on its error, so MustDerive(nil) returning a nine-field Statement IS Derive(nil) succeeding; the residual list dropped limitation 0's 'over real rows' qualifier, which is the whole content of the bullet. TWO MECHANISM NOTES. (1) MustDerive(nil).SectionTotal(0) chains a method onto a constructor result with NO CLOSURE, so the bare New(...).Method(...) shape works whenever the constructor is SINGLE-VALUED; a closure is needed only for a multi-valued return. (2) WIRE FORM, new: a struct renders inline as (struct{(v t),(v t),...} <fully-qualified>.Type) -- fields in DECLARATION ORDER, comma-separated, NO FIELD NAMES -- and an empty-but-non-nil slice renders (slice[] []<path>.Elem). A struct reply is therefore POSITIONAL, so renaming or reordering a field silently reshuffles it; prefer a closure returning named scalars when a reply will be parsed. STILL UNREACHED, unchanged and citing its attempt: no type in this package or p/cashflow has a field of type journal.Account or journal.Entry (measured by grep), so all seven of Derive's error paths, every checked-add overflow, ErrRowsNotOrdered, ErrNegativeTotals, ErrTooManyRows and the abnormality PRODUCER remain local-test evidence only.
Source
| Source file | onyx/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 bytes | yes — byte-identical |