Skip to Content
Fund AccountingPolicy IR & Economic Terms

Policy IR & Economic Terms

How does a fund’s LPA — “2.5% on committed capital during the investment period, stepping down 25 bps/yr to a 1.5% floor; 20% carry, escalating to 25% above 3×” — become something the close engines can execute deterministically? The answer is a typed intermediate representation (IR) stored directly as the terms payload — directly, because any separately-persisted “compiled” form is a second truth that can drift from the first. This page covers ADR-0019 (“the payload IS the IR”) and the IR schemas in packages/fund-economics/src/.

One truth: vehicle_terms_versions.payload

The canonical economic-policy layer is a single versioned store:

  • Where: vehicle_terms_versions.payload — typed Effect-Schema IRs from @opsdna/fund-economics, stored as schema-registered (fund_economic_policy_payload_registry), canonically-encoded, payload_hashed JSON.
  • Scoped by terms_kindlpa / side_letter / management_fee / waterfall / expense_allocation / pcap_policy, optionally by partner_class_id.
  • No persisted compiled step. “Compilation” is decode → validate → lower to engine inputs at run time, deterministic and pinned. The original design’s compiled engine tables were never fed by production code and were dropped (ADR-0019 §5) — the payload was the only load-bearing layer.

Effect Schema is the single definition language because one definition yields every projection: decode (validation), encode (the canonical bytes the determinism hashes cover), the TypeScript types the engines execute, the JSON Schema handed to extraction models, and the registry entry that rejects unregistered shapes.

The IR schemas live in packages/fund-economics/src/: management-fee-policy-ir.ts, waterfall-policy-ir.ts (WATERFALL_POLICY_SCHEMA_VERSION is 5 — selector branches × a nested carry_schedule over a shared MetricThreshold vocabulary, per ADR-0011’s marginal-carry amendment), expense-allocation-policy-ir.ts, partner-override-ir.ts, plus the shared grammar in economic-grammar.ts, period-selector.ts, and fee-rate-schedule-selector.ts.

Queryability is a projection, never a second store

“Find every fund where carry > 25%” is answered by fund_terms_index — flattened rows derived deterministically from the payload at approval time, rebuildable in full at any moment. It is read by search/reporting surfaces only. Engines never read it, enforced by the package-boundary scanner (a banned import for packages/fund-economics/src/runtime/ and the close builders) plus a posture test. The index answers “what is true now”; “what did we know at approval time” is answered by the versioned payload rows themselves.

One close consumes several approved terms versions concurrently (fee policy + waterfall + expense allocation + the LPA + side letters). So the pin is not a single FK but a link set: calculation_run_terms_versions — one row per terms version the run consumed, written at run creation, immutable thereafter, with a coverage invariant asserted by the derivation-chain meta-test. Replay semantics are unchanged: input_hash already pins the exact terms bytes; the link set makes the pinned thing navigable.

Profiles fail closed

The grammar stays fully general (other strategies someday); what a given fund may author is governed by its economic profile, default venture, which refuses what ADR-0011 rules out — preferred-return/catch-up state accounts and hurdle-shaped tiers (recognized by shape, regardless of authored rate) and excused-investor expense carve-outs. The behavior contract:

  • Refuse loudly: unsupported_for_profile validation diagnostics with paths — never silently dropped, never silently computed.
  • Capture faithfully: constructs the engine cannot yet run are labeled normalizable_but_not_executable (e.g. a carry conditioned on related-fund facts normalizes now and flips to executable once the fact source is wired).
  • Re-enabling a refused lever for a future strategy (a PE profile with a hurdle) is profile work plus engine validation, never a grammar change.

Implementation: packages/fund-economics/src/economic-profile-gate.ts; the executability firewall is executableApprovalIssues in policy-draft-envelope.ts, which today enforces feature support ∧ fact wiring (a draft needing an unwired fact reports non-empty missingFacts).

Ingestion: the model never composes the tree

The PDF lane extracts toward the same AST the wizard edits — two producers, one reviewed, evidence-cited draft. The load-bearing design points (ADR-0019 §7): clause segmentation → defined-term harvest (the document’s symbol table) → per-clause micro-extractions against small sub-schemas, every leaf anchored to an evidence span → deterministic assembly in ordinary tested TypeScript (which rejects unanchored leaves outright) → the validation pyramid → human review → approval. Span coverage is total: every span of an economically relevant section ends mapped, intentionally ignored, or review-blocking — an unresolved span blocks approval. Correctness is judged empirically by an end-to-end eval harness (page image → draft IR → Acceptance Lab close → statement tie-out), not in code review.

Pitfalls.

  • Never introduce a parallel unregistered JSONB terms shape — new primitives enter only via schema-version bump + registry + golden fixtures.
  • Never hand-feed a queryable terms table — queryability is a rebuildable projection, and an engine consulting fund_terms_index fails at lint time by design.
  • Never cite a fund_terms_index row as historical evidence — it is a current projection, full-replaced on rebuild.
  • Never encode economics on partner_role — carry rate, net/gross basis, and fee schedule are partner_class attributes resolving through vehicle_terms_versions (ADR-0011, DATA-MODEL rule 3).

Related: Application Records (what the lowered IR produces at close time), Entity Model (partner_class, vehicles, and who owns economics).

Last updated on