Skip to main content
CFDLAcademy
All chapters

Part 3 · Modeling judgment · Chapter 20 of 28

Packs: when and why

Everything in this course so far has been packless: models built from the core constructs alone, every claim written by hand. That was deliberate — you now know what every pack-generated structure is made of. This chapter adds the last major decision: when to bring in an industry pack, what it buys, what it costs, and how to adopt one without changing a number.

What a pack is

A pack is a domain's modeling knowledge, versioned and declared in one line — and its types are refinements of the language's own masters: CRE.Contract.PermanentDebt states refines = "Contract.Debt", so anything written against the master (a slice, a validation) reaches the pack's types without naming them. Declared in one line:

use pack "cre" version "0.1.0"

Four things arrive with it.

A typed vocabulary. The base ontology (Asset.Real, Party, …) extends with domain types — CRE.Asset.RealProperty, CRE.Asset.Unit, Credit.Asset.Tranche — each declaring the fields its kind of thing carries. Your entities become checkable against domain knowledge. A misspelled field on a typed entity is a compile error, with the pack's vocabulary in the message. A contract can also require that it attaches to the kind of thing it makes sense on.

Contracts. The construct this course has mentioned since chapter 2 and can finally show. A contract states a standard piece of domain economics by its terms, and the pack lowers it into the streams you would otherwise write by hand:

contract cre.lease on entity asset.tower {
  term 2026-07..2031-12
  terms {
    rent = 25000
  }
}

A few lines. on entity asset.tower names what the contract is written on: its subject, which owns the cash it lowers. Every contract names its subject, and a contract type states its roles, which a parties { … } block binds when the deal names them (docs/01 §8.1). What the lines replace, you can write yourself by now: the rent stream, its monthly schedule over the term, the amount from the rent. The pack's lowering does exactly that — visibly, in the IR, as ordinary streams — so a contract is shorthand you can audit, not a black box. Richer contracts carry richer terms: cre.lease states an occupancy ramp as three of its terms (lease_up_months, stabilized_occupancy, absorption_shape — the chapter-9 idiom, as terms), cre.permanent_debt a full amortizing loan, credit.loan a pool that lowers eleven named streams — interest, scheduled principal, prepayments, recoveries, servicing, defaults and more.

Domain validation. The pack checks what the core language cannot know: a lease missing its rent, an occupancy outside [0,1], a debt with a non-positive amortization. Each is refused at compile, in domain vocabulary, before any number exists; a required term left out is refused by name. Your deal is reviewed against the domain's checklist every time it compiles.

Categorization and domain metrics. Statements themselves are the language's: a model may declare its own (Part III), and even packless, results render a default entity-hierarchy statement rather than a flat list of streams. What the pack contributes is the content a domain statement needs. Its contracts declare what cash is — this stream is base rent, that one is debt service — by attaching a category to every stream they lower, so subtotals and derivations become structural. NOI, DSCR, and EBITDA arrive in results as named metrics with lineage, and the pack ships its own statements — the domain's pro forma — alongside any the model declares. This is what your packless models' hand-designed name taxonomy was approximating; the pack fills it in with the domain's judgment.

Four packs ship:

  • cre — more than twenty contract types in families: leasing (whole-property and unit leases, rollover, speculative leasing, ground leases, percentage rent), operations (revenue and expense lines, vacancy, property tax, incentives), development (budget lines, equity commitments, sponsor fees, the construction loan), debt (permanent, mezzanine, rate caps, mortgage insurance), capital (reserves and capital programs) and transactions (purchase, one exit on a stated basis, unit sales); plus six lease and loan option types.
  • credit — loans (level-pay, interest-only or bullet, fixed or floating), purchases, participations, structured notes, servicing, guarantees; a buyout and a clean-up call as option types.
  • energy — PPA, merchant, storage, capacity, O&M, ITC/PTC, MACRS shields, capex, debt, reserves.
  • opco — revenue/opex/capex lines, reinvestment, working capital, term debt, cash taxes, acquisition, three exit forms.

The capstone lives in cre; the reference part tables its terms.

Two tools for finding your way

A pack's vocabulary is large, and two tools of the cfdl-mcp server keep you from guessing at it (docs/09 §9).

lookup answers what a model may write before it is written. Asked for a pack, it returns the pack's entity types with their fields and lifecycles, each contract type with its roles, terms and lines, every name a contract's lowering publishes, the steps, events and accounts it carries, its categories, and the metrics, subtotals and statements the pack publishes after a run. When you are unsure whether a lease's rent is rent or rent_year, ask lookup, not memory.

skeleton starts a model from a whole deal rather than an empty file. Asked for a pack alone, it returns the pack's catalog: its named deal shapes, contract and entity types, each with the clues that say a deal has one. Asked for a shape — cre.development, credit.securitization, opco.lbo — it returns a model with that deal's structure, every number an assumption stated at the top with an illustrative value, so the model runs as it is, and beside it the same values as an inputs file and the run configuration it was compiled and run under. The capstone starts from a skeleton in the next chapter.

When packless is the better choice

The decision is not "packs when available." Packless wins in three real situations. The deal is genuinely novel — a structure the pack's contract list does not describe; forcing it through the nearest contract misstates the deal, and the core constructs exist precisely for this. The model is pedagogical or exploratory — you are finding out what the deal even is, and writing claims by hand is the finding out. The economics deviate from standard in ways that matter — a lease with a bespoke kicker the contract does not carry. Here the answer is often mixed: contracts for the standard bones, hand streams beside them for the bespoke flesh. Packs and hand streams compose freely in one model, and most mature models are exactly that mixture.

The real cost of a pack is a dependency on its judgment. Its lowering is the standard treatment and its validations are the standard checklist. Where your deal disagrees with standard, you must notice the disagreement. The pack makes standard cheap; it cannot make your deal standard.

Migration: the invariant refactor, again

Adopting a pack in an existing model is chapter 17's discipline at larger scale — a refactor that must not move cash. The sequence:

  1. Type the entities. entity asset tower : Asset.Real becomes : CRE.Asset.RealProperty. Compile: the ontology now checks your fields.
  2. Replace one storyline at a time. Delete the hand-written rent stream; state the cre.lease with the same term and rent. Run. The totals must match your packless version — the contract lowers to the streams you deleted.
  3. Compare in the results, not the source. Stream names change (the pack's lowering names its streams); the cash must not. The annual rollup is the comparison surface.
  4. Keep the bespoke. Whatever the pack cannot state stays as your hand-written claims, now beside contracts instead of instead of them.

The exercise below runs this migration on a small office model; the capstone then begins pack-first, because its deal is standard CRE and you now know exactly what its contracts expand into.

What can go wrong

A contract for a deal that is not that contract. A term may hold what was agreed even when the agreement is a formula. escalation = inputs.cpi + 0.005 on a lease states "CPI plus 50 basis points" directly, and free rent is a term of cre.lease_unit. Much that looks like structure is vocabulary too: a tenant's termination right is an option of type CRE.Option.Termination, one of six option types the pack declares, and a sale that repays a loan names it with pays_off. The boundary is a shape the pack's contracts and options simply do not have. When the deal's structure is not the contract's structure, that mismatch is information: write the claims by hand.

Fighting the validation. A pack refusal that seems wrong usually means your deal and the pack disagree about what is standard — which is worth knowing, not suppressing. Read the refusal as domain review, then decide deliberately: fix the model, or take that storyline packless.

Migrating without the invariant. Adopting a pack and correcting numbers in one pass produces a diff nobody can review. Two commits: the migration (cash identical), then the corrections (cash changed, on purpose, visibly).

Exercises

Exercise

The invariant migration

  1. Run the packless starter. Note the total.
  2. Declare the pack.
  3. Retype the entity.
  4. Restate both streams as contracts over the same term: cre.lease with its rent, and cre.opex_line with its amount, each on entity asset.office.
  5. Run again. Compare results, not source: stream names change, because the pack's lowering names what it generates — the cash must not.
  6. If the totals differ, the migration changed a claim. Find the claim before you trust either version.

Then break the model on purpose, once. Delete the rent line and read the pack's refusal. That message — the domain's checklist speaking — is most of what the migration bought you.

Loading exercise…

Then, on your own:

  1. In the exercise's solution, misspell a term (rnet) and read the pack's refusal, E1371, which names the near miss; then write the contract on entity a party and read that one. You are meeting the domain checklist you get on every future compile.
  2. From a deal you know, list its storylines and sort them: which are a pack contract verbatim, which are a contract plus a bespoke stream, which are irreducibly hand-written? That sorted list is a pack-adoption plan — and the fraction in the first bucket is a fair measure of how standard your deal really is.