Skip to Content
Fund AccountingLedger Kernel & Money

Ledger Kernel & Money Discipline

packages/ledger is the double-entry core: the kernel that turns source events into balanced, dimensioned journal entries, the money types that make float arithmetic unrepresentable — because a GL that can drift by a rounding cent is one an auditor cannot trust — and the ASC 820 disclosure builders. This page also covers where per-LP balances live (spoiler: in a projection, not a second ledger).

The kernel (packages/ledger/src/kernel.ts)

The kernel (LEDGER_CODE_VERSION = "m9-ledger-kernel-v1") operates on an in-memory LedgerState (accounts, books, dimensions, journal entries, source events, reconciliations — see schema.ts) and exposes posting plans: typed, pre-computed descriptions of a posting that carry their own provenance and rounding facts before any row is written.

// packages/ledger/src/kernel.ts export interface CapitalCallIssuedPostingPlan { readonly sourceEventId: string readonly sourceEventAmountCents: number readonly ruleSetId: string | null readonly ruleSetVersion: number readonly allocatorKind: RuleSet["allocator_kind"] readonly materialityThresholdCents: number readonly allocationRuleId: string readonly allocations: ReadonlyArray<ContributionAllocation> // per-investor, // each with amountCents, committedAmountCents, roundingRemainderCents readonly inputHash: string }

A plan pins the rule-set id and version, the allocation rule, the per-investor split including each investor’s rounding remainder, and an inputHash — so the posting is replayable and the largest-remainder cent distribution is auditable line by line. Domain-specific posting logic lives in src/posting-compilers/ (fair-value adjustments, capital calls, etc.), with preflight checks in src/posting-preflight/ and leg-shape/dimension discipline asserted by posting-leg-shape.ts, account-dimension-rules.ts, and dimension-assignment-provenance.ts.

Two other kernel disciplines worth knowing:

  • Deterministic account ids. Canonical accounts get uuid-v5 ids derived from canonical:<kind> namespaced under the accounting entity (canonicalCatalogAccountId), so every seeding path, posting dispatcher, and available-accounts list mints one idempotent id per (entity, kind) — and a later SPV gets its own chart, not a collision. The id is opaque; the kind lives in canonicalAccountKind, never parsed out of the id.
  • Dimension snapshots are verified. Journal-line dimension snapshots are checked against normalized links with a typed issue taxonomy (missing_dimension_value, snapshot_value_mismatch, …) — denormalization for read speed, with an invariant checker instead of blind trust.

Money: branded bigint, never number

packages/ledger/src/money.ts defines the canonical money/ratio/quantity types. Backing storage is bigint at a fixed decimal scale — never number — and the brand makes raw bigints and numbers unassignable without a constructor:

export const MONEY_SCALE = 4n as const // sub-cent precision, ×10^4 export const RATIO_SCALE = 12n as const // bp-fractional precision, ×10^12 export type Money = bigint & { readonly [MoneyBrand]: void } // Money.mul: product is at scale 16; round back to 4, default banker's. mul: (m: Money, r: Ratio, mode: RoundingMode = "half-even"): Money => { ... } // Money.divToParts: split total across weights so sum(parts) === total // EXACTLY — largest-remainder method, ties broken by ascending index; // signed totals handled by reflection so reversals conserve too. divToParts: (total: Money, weights: readonly Ratio[]): Money[] => { ... }

divToParts is the cent-remainder rule that makes pro-rata allocations conserve to the cent — the same discipline the posting plans record as roundingRemainderCents. The fund-economics runtime uses the parallel exact-decimal layer (packages/fund-economics/src/exact-decimal.ts over MoneyValue/RatioValue from @opsdna/fund-domain), and evaluator traces embed each value’s canonical-decimal projection plus the decimal-runtime version so trace arithmetic is provably reproducible.

Where per-LP balances live: the PCAP projection (ADR-0010)

Per-LP capital accounts look like they live in two places. They don’t — there are three layers, not two competing stores:

The two canonical homes hold two kinds of fact, not two copies of one fact: a carry accrual is a net-0 inter-partner reallocation that is off the P&L and absent from posted journals (ADR-0009) — the subledger is its single correct home. The projection exists so the read model has one uniform place to read, and drift is non-silent by construction: every projected row carries source_hash + source_watermark, so a changed canonical source makes the row known-stale and rebuildable, never quietly wrong.

Statements (packages/fund-statements-builder, consumed by the Acceptance Lab’s fund-statements-builder.ts) render P&L, balance sheet, and per-LP PCAP from the GL and these projections; ASC 820 disclosure sheets (asc-820-level-3-rollforward.ts, soi-sheet.ts, partners-capital-sheet.ts in packages/ledger/src/) derive from the marking register — see Accounting Foundations.

Pitfalls.

  • amount: number is the anti-pattern the Money brand exists to kill. Never route monetary values through IEEE floats; construct via moneyFromCents/moneyFromString and keep arithmetic in the branded ops.
  • Never treat a projected PCAP row as authoritative, and never reconcile two projected rows against each other as if either were truth (ADR-0010 rule 1).
  • Each component has exactly one canonical home. “Dimensioning a compiler” means changing a component’s canonical home and adding a journal_line producer — never adding a second per-LP write path.
  • Posted journal lines are immutable; migrations are forward-only. Schema changes to the ledger need the real-Postgres integration lane, not just pglite verify (see Persistence).
Last updated on