Skip to content

.uwx.md — UW Markdown Extended Format Specification ​

Version 1.1 | April 2026 ​


A .uwx.md file is the canonical, lossless, human- and machine-readable representation of a CRE deal underwriting. It is simultaneously a document (readable by a lender), a data file (parseable by tools), a pipeline artifact (written to by agents in sequence), and a context bundle (dropped into an AI assistant for instant deal literacy). It replaces the Excel underwriting model as the source of truth, and replaces the Word credit memo as the human-readable output — by being both at once.


Naming: "UW Markdown" the format, .uwx.md the file ​

The standard is called UW Markdown, and a deal record is colloquially "a UW Markdown document" — the same way people say "a Word document" while the file on disk is .docx. The precision matters when it matters:

TermMeans
UW MarkdownThe standard as a whole — this spec, the protocol, and the representations below. Use it in prose.
UWX / .uwx.mdThe complete underwriting record, specified by this document: full section model, append-only provenance, calc inputs. Lossless.
UW Lite / .uw.mdA constrained, human-readable summary, specified separately in UW_LITE_SPEC_v1.md. Semantics come from <!-- uw:path --> anchors. Explicitly lossy.

Where the Word analogy stops: .doc is a superseded predecessor, whereas UW Lite is neither older nor deprecated. It is a current, deliberately lossy view of a deal, with its own specification and its own job. UWX is what a deal is; Lite is one way of showing it. The projection UWX → Lite MUST report every path it omits (RFC 0017).

Structured content carrying the legacy .uw.md extension predates this split. It remains readable — detected by sniffing rather than by extension — but extension no longer implies structure, and a byte-identical .uwx.md sibling is the migration output. Do not write new structured files as .uw.md.


Conformance language ​

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. For implementation requirements (what tools must do with these bytes), see the companion UW_PROTOCOL_v1.md.

Section count ​

Part IV registers 28 numbered subsections (§ 4.0 – § 4.27): 21 standard data sections (§ 4.0 – § 4.20), the extension-section meta-spec (§ 4.21) that defines the x_ namespace for non-standard content, the gaps inventory (§ 4.22), the mixed-use components section (§ 4.23), the capital_stack section (§ 4.24), the lease_up_schedule section (§ 4.25), the cash_flow_series section (§ 4.26), and the distribution_waterfall section (§ 4.27). When this and the protocol document refer to "the 21 standard sections" they mean § 4.0 through § 4.20.


Part I — Design Philosophy ​

1.1 The Core Problem This Solves ​

An Excel underwriting model is a calculation engine with no portability. You cannot drop it into a terminal. An AI tool cannot read it without preprocessing. It has no provenance — you cannot tell who changed a cell, when, or why. It cannot be validated programmatically. It cannot render itself into a lender package.

A .uwx.md file solves this by being structured enough for machines while staying readable enough for humans. The format is the deal. Everything that lives in Excel, Word, and a banker's email thread lives in one file.

1.2 Design Principles ​

Lossless. Every number, assumption, flag, narrative, and audit trail that would exist in an institutional underwriting model has a defined home. Nothing is omitted to keep the file small.

Dual representation. Every section carries both prose (human-readable narrative, tables) and a structured JSON data block (machine-parseable). Either layer can stand alone; together they are the complete record.

Append-only by default. Tools and agents add to the file — they do not rewrite existing sections. History is preserved inline. A compact operation removes superseded blocks when the file is ready to archive.

Provenance on every block. Every data block carries a _meta object that records who wrote it, when, with what confidence, and whether it requires human review. No fact in the file is anonymous.

Source-tagged assumptions. Every assumption carries a source tag: scenario_default | market_data | ai_extracted | user_override | investor_profile | agent_computed. Users and reviewers always know whether a number came from AI, from market data, or from their own input.

Pipeline-stateful. The frontmatter tracks which pipeline stages have run. A tool reading a .uwx.md knows immediately what work has been done, what is pending, and what is blocked.

Toolchain-agnostic. The format is plain Markdown + fenced JSON. It can be read by any text editor, any Markdown renderer, any JSON parser. No proprietary tooling is required to read it. Proprietary tooling (Bancroft agents, uwmd CLI) is required to generate sections correctly, but not to read them.

1.3 Relationship to the Bancroft Agent Suite ​

Each Bancroft layer corresponds to one or more .uwx.md sections it is responsible for:

Bancroft LayerWrites Sections
L0 — Document Ingestionrent_roll, operating_statement, lease_schedule
L1 — Screeningscreening, preliminary_sizing
L2 — Underwritingnoi_model, valuation, market_analysis, borrower_sponsor
L4 — Structuringdebt_structure, sources_uses, covenants
L5 — Compliancecompliance
L6 — Risk Ratingrisk_assessment
L7 — AssemblyReads all sections, renders output artifacts
L9 — PortfolioAppends to operating_statement, updates risk_assessment

The wizard populates: deal_context, property, rent_roll (draft), debt_structure (terms), borrower_sponsor (form inputs), dcf (hold period inputs).

The deterministic engine (calculations.ts, dcfEngine.ts) writes: noi_model, dcf, stress_tests, validation, custom_calculations (result fields only — definitions are user-authored).

Users and AI assistants write: deal_context (narrative), custom_calculations (definitions), custom_scenarios, x_* extension sections.


Part II — File Grammar ​

2.1 File Structure ​

A .uwx.md file is a UTF-8 encoded plain text file with the extension .uwx.md. Its structure, top to bottom:

[1] YAML Frontmatter          ← delimited by --- / ---
[2] Deal Header               ← H1, human-readable property title
[3] Deal Summary Table        ← generated after L7; quick-read metrics
[4] Sections                  ← ordered by section registry (§ 4.x)
    [4a] Section Header       ← H2 with section name and anchor
    [4b] Prose Block          ← human-readable narrative and/or tables
    [4c] Data Block(s)        ← fenced JSON with uw: annotation
    [4d] Divider              ← --- between sections
[5] Pipeline Log              ← always last; append-only execution history

Sections MUST appear in the canonical order defined in Part IV. Parsers MUST tolerate out-of-order sections by scanning for uw:section= annotations rather than relying on position.

2.2 Frontmatter Schema ​

The frontmatter is YAML. All fields shown; * = required.

yaml
---
# ── Identity ──────────────────────────────────────────────
uw_version: "1.0"                   # * format version
deal_id: "uw_YYYY_[hash8]"          # * globally unique deal identifier
deal_name: "string"                 # human label, e.g. "Parkview Apts — Phoenix"
created: "ISO8601"                  # * timestamp of file creation
last_modified: "ISO8601"            # * updated on every write
locale: "en-US"                     # display locale (RFC 0001); absent = en-US.
                                    # Governs chat/summary/report display ONLY —
                                    # content stays canonical (JSON numbers,
                                    # ISO dates, fraction rates). Registered
                                    # values: en-US, en-GB, de-DE, fr-FR,
                                    # ja-JP, zh-CN (Protocol §III.1a). An
                                    # unregistered value is LOC-01: display
                                    # renders are refused, never silently
                                    # produced in a different locale.
currency_code: "USD"                # optional document monetary identity
                                    # (RFC 0046); three uppercase ASCII letters.
                                    # It controls currency identity only; locale
                                    # still controls numeric separators. Absent
                                    # preserves the locale's conventional symbol.

# ── Property Identity ─────────────────────────────────────
property_address: "string"          # * full street address
city: "string"
state: "XX"                         # 2-char state code
zip: "string"
asset_class: "multifamily | office | retail | industrial | self_storage | hospitality | mixed_use | senior_housing | student_housing | land | <namespaced custom identifier, see §2.2a>"
asset_subtype: "string | null"      # e.g. "garden_style", "strip_center", "warehouse"
loan_type: "permanent | bridge | construction | value_add | refinance"
scenario: "stabilized_acquisition | ground_up_development | value_add | lease_up | nnn_single_tenant | lihtc_section8 | house_flip | commercial_flip | property_conversion | build_to_rent | distressed_reo | land_banking"

# ── Pipeline State ────────────────────────────────────────
# Layer keys correspond 1:1 to BANCROFT_LAYERS in the reference library.
# L3 is intentionally reserved for a future pre-structuring layer and is
# omitted from v1; conforming files MUST NOT define L3_* keys. A layer key
# absent from pipeline_state is treated as "pending" by default. Use
# "skipped" only when a layer is explicitly bypassed for this deal
# (e.g. a refinance with no L0 ingestion).
pipeline_state:
  L0_ingestion:    "complete | in_progress | pending | skipped | failed"
  L1_screening:    "complete | in_progress | pending | skipped | failed"
  L2_underwriting: "complete | in_progress | pending | skipped | failed"
  L4_structuring:  "complete | in_progress | pending | skipped | failed"
  L5_compliance:   "complete | in_progress | pending | skipped | failed"
  L6_risk:         "complete | in_progress | pending | skipped | failed"
  L7_assembly:     "complete | in_progress | pending | skipped | failed"

# ── Overall Status ────────────────────────────────────────
status: "draft | in_progress | complete | flagged | archived"
deal_stage: "scope | screening | term_sheet | full_underwrite | credit_approval | closing | monitoring"
recommendation: "pending | approve | approve_with_conditions | decline | null"

# ── Key Metrics (denormalized for quick parsing) ──────────
# These mirror the financial_model section; kept here for fast lookup
# without parsing the full document. Updated by the engine after each run.
quick_metrics:
  purchase_price: null
  loan_amount: null
  noi_underwritten: null
  dscr: null
  ltv: null
  debt_yield: null
  cap_rate: null
  irr_projected: null
  equity_required: null

# ── Active Flags (denormalized) ───────────────────────────
# Mirror of validation section flags; kept here for pipeline routing
flags: []
blocking_flags: []

# ── Rendering ────────────────────────────────────────────
tier: "screener | analyst"           # determines which sections render to output PDF
institution_config_id: "string | null"  # Bancroft institution profile used

# ── Provenance ───────────────────────────────────────────
created_by: "wizard | import | manual | agent"
source_documents: []                 # list of filenames ingested via L0
---

Pipeline stages. The deal_stage enum advances monotonically as the deal progresses. The scope stage indicates a back-of-napkin readiness level: a scope-stage file MAY consist almost entirely of provisional blocks (see Part III §3.4 _meta.provisional) and represents triage rather than commitment. The minimum content for scope is a property section with at least an address, an asset class, and one of units or asking_price; everything else MAY be filled in from the fallback cascade (see Protocol §IX). Advancing past scope requires real documents, not better assumptions — producers MUST re-stamp defaulted blocks with observed data before setting deal_stage: screening or later.

2.2a Asset-class identifiers (RFC 0003) ​

asset_class is one of the ten builtin values, or a module-declared custom identifier.

asset_class := builtin | custom
builtin     := 'multifamily' | 'office' | 'retail' | 'industrial'
             | 'self_storage' | 'hospitality' | 'mixed_use'
             | 'senior_housing' | 'student_housing' | 'land'
custom      := segment ('.' segment){2,}
segment     := [a-z][a-z0-9_]*

The builtin set stays closed. It answers "what does data_center mean?" by not allowing the question, and that is worth keeping. What RFC 0003 adds is a second, namespaced space so that an adopter who needs a class the standard does not have is not forced to lobby for a spec bump, misuse an existing class (a data center as industrial, which then fails industrial's validations), or fork.

Three segments minimum. com.data_center reads as a namespace with no owner, and two segments is where squatting on a short prefix starts to look attractive. Reverse-DNS fixes ownership at the identifier, so two implementations can never disagree about whose com.example.data_center this is.

An identifier that is neither a builtin nor a well-formed custom identifier is INVALID-ASSET-CLASS-001 (error).

Builtin names are reserved in the final segment. A custom identifier whose last segment equals a builtin name — say com.example.multifamily — is INVALID-ASSET-CLASS-002 (error). The namespace already prevents collision, so this is stricter than correctness requires; it is here because such an identifier reintroduces exactly the ambiguity the closed enum removes, and because any host that does suffix matching (a host bug, and one that will happen) would conflate the two. com.example.multifamily_senior remains available and says more.

A namespaced class obliges the file to name its modules. A document whose asset_class is custom SHOULD carry a modules list naming what declares it:

yaml
asset_class: com.example.data_center
modules:
  - id: com.example.datacenters
    version: ">=0.1.0 <1.0.0"

Each entry is either a bare module id or an object with id and an optional semver-range version. Omitting the list is MOD-DEPENDENCY-UNDECLARED (warning, not error): the document is well-formed, and a host that happens to hold the module reads it correctly. The cost falls on every other reader, who can then say only that something is missing and not what to load.

Resolution — what a host does when it encounters a custom class — is Protocol §X.2. It is deliberately not in this document: the identifier is a property of the file, while resolution depends on what a particular reader has loaded.

2.3 Section Headers ​

Each section uses an H2 header with a machine-readable anchor:

markdown
## Property {#property}

The anchor (e.g., {#property}) is the section ID used for internal linking and cross-referencing. It matches the uw:section= annotation on data blocks within that section. Parsers use the annotation, not the header text.

2.4 Data Block Annotation Syntax ​

Every data block is a fenced JSON code block. The opening fence carries a structured annotation:

```json uw:section=<section_id> [key=value ...]

Required annotation keys:

KeyValuesDescription
uw:sectionsection ID from registryIdentifies which section this block belongs to

Optional annotation keys (parsed from fence tag for fast access; also present in _meta):

KeyValuesDescription
sourcemanual, agent/L0-01, system/calculations.ts, etc.Who wrote this block (actor grammar, §2.6)
tsISO8601Timestamp of this block
vintegerVersion number within this section (starts at 1)
supersededtrueMarks a block that has been replaced by a newer version
variantstringFor sections with multiple instances (e.g., t12, t3, budget)
confidencehigh, medium, lowQuick confidence signal

Full example fence tag:

```json uw:section=rent_roll source=agent/L0-01 ts=2026-04-24T10:12:00Z v=1 confidence=high

2.5 The _meta Object ​

Every data block MUST contain a _meta object as its first key. This is the canonical provenance record.

json
"_meta": {
  "section": "rent_roll",
  "version": 1,
  "superseded": false,
  "source": "agent/L0-01",
  "agent_id": "L0-01",
  "agent_version": "1.0.0",
  "actor": "system",
  "timestamp": "2026-04-24T10:12:00Z",
  "confidence": "high",
  "human_review_required": false,
  "flags": [],
  "input_hash": "sha256:abc123...",
  "notes": null
}
FieldTypeDescription
sectionstringSection ID
versionintegerMonotonically increasing per section per file
supersededbooleanTrue if a newer version exists in this file
sourcestringWho wrote the block — actor identifier (see 2.6)
resolutionstring(optional) How the value was resolved — canonical tag (see 2.6)
agent_idstringBancroft agent code, or "wizard", "engine", "user"
agent_versionsemverVersion of the agent/tool that produced this block
actorstring"system" or user identifier
timestampISO8601When this block was written
confidenceenum"high" / "medium" / "low"
human_review_requiredbooleanWhether a human should review before advancing pipeline
flagsstring[]Flags raised by this specific block
input_hashstringHash of the inputs that produced this block (for reproducibility)
notesstringFree-text notes from the agent or user

Blocks MAY also carry the optional fields partial, provisional, field_overrides, content_hash, parent_hash, and signature. See Part III §3.4 for their semantics. These fields are additive: a block omitting them is well-formed, and validators MUST NOT reject blocks for their absence.

The flat shape above is the only valid _meta form for uw_version: "1.x" files. Format 2.0 (UW_FORMAT_SPEC_v2.md, RFC 0009) reorganizes _meta into nested sub-objects (provenance / quality / lifecycle / integrity); that nested shape belongs to files declaring uw_version: "2.0" or later. A file's uw_version is global and decides the shape for every block in it: a nested _meta in a 1.x file is a META-V2-IN-V1 error, and a flat _meta in a 2.0 file is META-V1-IN-V2. uwmd migrate --to-v2 converts a whole file. The v2 document is a delta specification: this document continues to govern 2.0 files except where it amends it.

2.6 Source Identifiers ​

Rewritten by RFC 0031. _meta.source used to be asked to carry two unrelated facts — who wrote the block, and how its value was resolved — and this section used to answer with two vocabularies that reconciled with neither each other nor the executable policy table. The facts now live in two fields.

_meta.source names the actor — who wrote the block. It MUST be one of:

manual | agent/<id> | document/<id> | system/<id> | institution/<id>

where <id> matches [A-Za-z0-9][A-Za-z0-9._-]*. The namespace set is closed at format 1.x (the runtime registry is ACTOR_NAMESPACES in protocol.ts); / is the sole delimiter. These are exactly the patterns BUILTIN_EDIT_POLICIES matches, so every well-formed source resolves a specific edit policy (Protocol §V.3). Examples: agent/L0-01, document/om.pdf, system/calculations.ts, institution/threshold-override.

The colon forms this section previously listed (agent:L0-01, engine:calculations.ts, user:override, import:filename.pdf, …) are retired. They differ from the policy patterns only in delimiter, which made them a trap: a producer following this section literally wrote blocks that resolved no specific policy and whose writes were classified incorrectly. A validator reports them as SRC-01 (warning).

_meta.resolution names the resolution method — how the value was produced. It is OPTIONAL and holds one canonical tag from SOURCE_TAGS (@uwmd/core); the cascade tags are normative for cascade resolution (Protocol §V.7 — the authoritative table). Producers stamping a value resolved by the fallback cascade MUST use the tag of the cascade step that produced the value.

TagMeaning
user_inputThe user typed the value (initial entry).
user_overrideThe user explicitly overrode a prior value.
manualA human-authored value, method not otherwise classified.
inherited_assumptionInherited from an ancestor in the composition DAG (Protocol §V.7.1).
investor_profileResolved from the active investor profile / buy box.
market_dataResolved from a market-data lookup at write time.
market_data_acceptedA market observation an analyst explicitly accepted as the underwritten value (RFC 0022 §4).
ai_extractedExtracted from a source document by an AI agent.
agent_computedComputed by an agent from prior agent outputs.
asset_class_defaultPulled from the published asset-class default table for the deal's asset class.
scenario_defaultA value derived from a named scenario in this file or institution config.
global_defaultPulled from a non-asset-class fallback table.
system_defaultHardcoded constant in the reference library or institution config. Producers SHOULD avoid relying on this layer for normative values.

Both fields MAY be present; they answer different questions. A block written by agent L6-01 from the asset-class default table is:

jsonc
"_meta": { "source": "agent/L6-01", "resolution": "asset_class_default" }

field_overrides[].source / field_overrides[].resolution carry the same split at leaf granularity (§3.4).

Read-time interpretation (transition). A reader encountering a canonical SOURCE_TAGS value in _meta.source MUST interpret it as resolution, and MUST treat the actor as absent rather than inventing one — the value can still say how it was resolved, but it no longer passes for who wrote it (in particular it satisfies no authority class in Protocol §V.3). Validators report it as SRC-02 — a warning for all of format 1.x, an error at 2.0. uwmd migrate --source-tags rewrites a file mechanically.

2.7 Update Semantics ​

When an agent or tool updates a section, it:

  1. Locates the most recent non-superseded block for that uw:section
  2. Sets "superseded": true in that block's _meta and in the fence tag
  3. Appends a new block with version = previous_version + 1

This means the file grows; history is always recoverable. The canonical current value of any section is the last block where _meta.superseded === false for that uw:section.

The uwmd compact command strips superseded blocks and leaves only current versions.

2.8 Multi-Variant Sections ​

Some sections have multiple valid instances at the same time (not superseded, just parallel):

  • operating_statement — may have t12, t3, budget, ytd variants
  • stress_tests — may have named scenarios
  • due_diligence — sub-documents (appraisal, environmental, title) are separate sub-sections

For multi-variant sections, the fence tag carries a variant= key:

```json uw:section=operating_statement variant=t12 source=agent/L0-02 ts=... v=1

Parsers collect non-superseded variants by their declared variant keys.

Signed block roles (RFC 0040). Any active block MAY carry one optional _role beside _meta, drawn from primary, senior, junior, summary, detail, component. This scalar is content covered by the existing block hash, not provenance metadata; _meta.role is not an alias. Primary names the generic statement, senior/junior the debt position, detail/summary its level of aggregation, and component a part of the property. Roles are allowed on any section, including a standalone block. Invalid values (including null or arrays) MUST raise ROLE-01 in every active block. Superseded history is not reclassified. Only a trusted host or explicit human assignment may set a role; agent content MUST NOT invent it. See §5.3 for selection and Protocol §V for editing. Parse and representation conversion MUST preserve this annotation.


Part III — Provenance Model ​

3.1 Assumption Source Hierarchy ​

Non-normative since RFC 0031. The normative precedence for assumption values is the fallback cascade in Protocol §V.7 (CASCADE_ORDER in protocol.ts): explicit user decisions first, then inherited assumptions, then the investor profile, then market data, then the default tables. Where an earlier revision of this section and §V.7 disagreed — this section ranked market data above the investor profile, §V.7 the reverse — §V.7 wins: a standing investor profile is a declared decision, a market lookup is an observation, and a declared decision outranks a scraped comp.

This section also used to rank ai_extracted and agent_computed, which are not cascade steps at all — they describe how observed data entered the file, not a fallback layer. Those rankings are removed rather than reinterpreted.

The assumptions section (§ 4.16) records every assumption with its resolution method. The UI displays source badges. A producer resolving a missing value walks the §V.7 cascade in order and takes the first step that yields one.

3.2 Confidence Levels ​

LevelMeaning
highDeterministic calculation or direct extraction from a clearly structured source document. Should be trusted.
mediumAI-extracted from an ambiguous source, or derived from a benchmark range midpoint. Verify key decisions against this.
lowEstimated, inferred, or default value with no source document. Flag for human review before advancing.

A section with any low confidence field sets human_review_required: true in _meta.

3.3 Human Review Gates ​

The pipeline respects two types of human review requirements:

Soft gate (human_review_required: true on a block): The pipeline continues but flags the deal for human attention before the next stage.

Hard gate (a blocking_flags entry in frontmatter): The pipeline halts. No further agents run until the flag is cleared by a human. Examples: OFAC match, appraised value > UW value by >10%, Phase II environmental required.

3.4 Optional Integrity / Quality Fields ​

Beyond the required and recommended _meta fields described above (§2.5), blocks MAY carry the following optional fields. A block lacking any of these fields is well-formed; validators MUST NOT reject blocks for omitting them.

FieldTypeMeaning
partialbooleanThe block is present but at least one field inside it is missing or unknown. When partial: true, an enumeration of which paths and why SHOULD be provided in field_overrides.
provisionalbooleanThe entire block is a placeholder, derived from defaults rather than observed data. Stronger signal than confidence: 'low'. Downstream consumers SHOULD label outputs derived from provisional blocks accordingly.
field_overridesarrayPer-field overrides where the block-level confidence, source, resolution, or annotations do not apply uniformly. Each entry has a dot-notated path relative to the block's content root and may carry its own confidence, source (actor, §2.6 grammar), resolution (canonical tag, §2.6), reason (`illegible
content_hashstringThe SHA-256 hash of the block's canonicalized JSON content. The canonicalization rule (RFC 8785 JCS, with _meta.content_hash and _meta.signature removed before hashing) is defined in Protocol §IX.2. Producers MAY emit; consumers MAY ignore.
parent_hashstring | nullThe content_hash of the block this one supersedes. null on a chain root. Required only when participating in an integrity-checked supersede chain — once any block in a supersede chain carries content_hash, every later block in that chain MUST carry both content_hash and parent_hash.
signatureobjectA detached signature over the block's signing input, for deployments that need cryptographic chain of custody. Fields: alg (ed25519 | es256 | es384), kid, sig (base64url, unpadded), signed_at (ISO 8601), optional v. Requires content_hash — a signature with no hash beside it commits to nothing (INT-05). Defined normatively in Protocol §V.11.

Precedence with field_overrides. A field_overrides entry whose path matches a field replaces, for that field only, the block-level confidence, source, and resolution — the leaf wins over the block for its path. The block-level values still apply to every field not enumerated. This produces two surfaces for expressing confidence; the consolidation lives in the v2 _meta reorganization (RFC 0009).

Path syntax. field_overrides[].path uses dot-notation with bracketed array indices (e.g. units[7].current_rent). Paths whose key segments contain a literal . are not expressible; no current section schema uses dotted keys.

Integrity opt-in. A file containing zero content_hash fields is indistinguishable from a pre-integrity file; chain verification is a no-op. To benefit from chain verification, a producer SHOULD stamp content_hash on every block it writes once it adopts the integrity path.

Signing opt-in. signature is optional on top of an optional field: it is meaningful only where content_hash is already stamped, and reading, validating, editing, and computing over a .uw.md never require cryptography. A consumer with no key store MUST treat a signature as opaque metadata and MUST report it as present but unchecked rather than as verified. See Protocol §V.11.

3.5 confidence vs human_review_required ​

confidence and human_review_required are orthogonal:

  • confidence is a quality estimate of the data ("how much do we trust the value?").
  • human_review_required is a workflow gate ("must a human review this before the file can advance to the next stage?").

High-confidence data MAY require review (e.g. for compliance audit). Low-confidence data MAY NOT require review (e.g. a placeholder filled in while drafting that the producer plans to overwrite). Validators MUST NOT infer one from the other; the existing META_LOW_CONFIDENCE_NO_REVIEW_FLAG issue is informational only.


Part IV — Section Registry ​

Each section entry specifies: ID, canonical header, purpose, who writes it, required fields, dependencies, and complete JSON schema.

Universal field — _notes: Every data block in every section (standard and custom) accepts a top-level _notes string field immediately after _meta. This field is always optional, never validated, and never consumed by agents or the engine. It is a free-text annotation written by users or agents to explain context, flag concerns, or leave instructions for the next reader — human or AI. Example: "_notes": "Tax assessment will reset at sale — current figure understates by ~$40k. Run revised NOI before credit committee." Renders display _notes as a footnote on the relevant section.

Reference path notation: To reference a live value from another section within custom calculations or custom scenarios, use dot-path notation: {section_id}.{field_path}. Array indices are supported: dcf.annual_cash_flows[0].net_cash_flow_levered. If the referenced value does not exist, the result is null and status is awaiting_inputs.

Explicit period references (RFC 0041 / Protocol §VIII.2a): safe calc expressions and resolvePeriodPath can address a registered series by identity, for example [email protected]_operating_income. Holding years, absolute months/quarters and dates remain distinct. Generic object paths and metadata pointer strings retain their existing literal-key semantics. The protocol defines duplicate refusal, variant context and PS-01/02/03 diagnostics. No field is moved and no period is inferred from row position.


§ 4.0 — Deal Context ​

ID: deal_context
Header: ## Deal Context {#deal_context}
Purpose: The narrative layer. Describes the deal in plain language — what it is, why it's being considered, what the goal is, and any special circumstances that a structured underwriting model cannot capture. This section is the first thing an AI assistant reads to orient itself. It is the only section the user is always expected to write in their own voice.
Written by: wizard (structured form fields), user (free narrative prose), agent/L7-01 (AI-synthesized summary — appended as a second block, never replaces user narrative)
Required for pipeline stage: All stages (prose block optional; structured block required for screening+)
Dependencies: None

The Deal Context section has two distinct layers:

  1. User-authored narrative — written in the prose block, before the data block. No format requirements. Can be one sentence or several paragraphs. This is where the user explains the story of the deal: the relationship context, the market timing, the specific constraint they're working within, the reason they're looking at this asset class, the gut feeling that brought this deal to the table. No schema can capture this — it lives here.

  2. Structured data block — the machine-readable distillation of that context into typed fields. Tools route on these fields, AI assistants read them to calibrate their analysis, and the render engine uses them to frame the output package.

The ai_synthesis sub-object is populated by agent/L7-01 after the full underwriting runs. It summarizes what the data actually shows in one paragraph, identifies strengths and risks from the model's perspective, and gives an overall impression. It is appended as a separate versioned block — the user's narrative is never touched.

json
{
  "_meta": { "...": "see §2.5" },
  "_notes": null,

  "deal_summary": "string | null",

  "investment_thesis": "string | null",

  "acquisition_rationale": "string | null",

  "value_creation_strategy": "string | null",

  "hold_strategy": "exit_at_stabilization | long_term_hold | develop_and_sell | refinance_and_hold | 1031_exchange | portfolio_addition | flip | other | null",

  "exit_strategy_description": "string | null",

  "deal_goal": "string | null",

  "special_circumstances": "string | null",

  "known_risks_user_identified": [],

  "known_opportunities_user_identified": [],

  "deal_history": "string | null",

  "time_constraints": {
    "loi_deadline": null,
    "due_diligence_period_days": null,
    "close_deadline": null,
    "hard_deadline": false,
    "notes": null
  },

  "competitive_situation": "string | null",

  "lender_preferences": {
    "preferred_lender_type": "bank | credit_union | cmbs | agency | life_co | debt_fund | private | sba | no_preference | null",
    "preferred_loan_type": "string | null",
    "rate_ceiling_pct": null,
    "min_io_period_months": null,
    "max_recourse": "full | partial | non_recourse | null",
    "notes": null
  },

  "investor_context": {
    "portfolio_role": "core | core_plus | value_add | opportunistic | null",
    "target_market_fit": "string | null",
    "buy_box_fit": "strong | moderate | marginal | exception | null",
    "buy_box_exception_reason": "string | null",
    "tax_considerations": "string | null",
    "depreciation_strategy": "string | null"
  },

  "deal_tags": [],

  "ai_synthesis": {
    "generated": false,
    "summary": null,
    "key_risks_identified": [],
    "key_strengths_identified": [],
    "overall_impression": "strong | adequate | marginal | weak | null",
    "generated_by": null,
    "generated_at": null
  }
}

§ 4.1 — Property ​

ID: property
Header: ## Property {#property}
Purpose: Physical asset description, identity, and condition.
Written by: wizard:step_1, wizard:step_2, agent/L1-01 (normalizes)
Required for pipeline stage: All stages
Dependencies: None

json
{
  "_meta": { "...": "see §2.5" },
  "address": {
    "street": "string",
    "city": "string",
    "state": "XX",
    "zip": "string",
    "county": "string | null",
    "apn": "string | null",
    "legal_description": "string | null",
    "coordinates": { "lat": 0.0, "lng": 0.0 }
  },
  "asset_class": "multifamily | office | retail | industrial | self_storage | hospitality | mixed_use | senior_housing | student_housing | land",
  "asset_subtype": "string | null",
  "year_built": 0,
  "year_renovated": null,
  "total_units": null,
  "total_nra_sqft": null,
  "rentable_square_feet": null,
  "gross_leasable_area": null,
  "net_rentable_square_feet": null,
  "rentable_units": null,
  "keys": null,
  "total_beds": null,
  "gross_acres": null,
  "usable_acres": null,
  "entitled_units": null,
  "land_area_sqft": null,
  "land_area_acres": null,
  "stories": null,
  "building_class": "A | B | C | D | null",
  "construction_type": "wood_frame | masonry | steel | concrete | mixed | null",
  "parking_spaces": null,
  "parking_ratio": null,
  "parking_type": "surface | structured | garage | covered | null",
  "amenities": [],
  "condition": "excellent | good | average_good | average | fair | poor | null",
  "deferred_maintenance_est": null,
  "recent_capex_description": null,
  "recent_capex_amount": null,
  "zoning": null,
  "flood_zone": null,
  "opportunity_zone": null,
  "hud_qualified_census_tract": null,
  "environmental_concerns_noted": false
}

Size intensives (RFC 0027). A property block MUST state the size intensive its asset class uses (the primary size field, per Protocol §XIII.1). It MAY state any of the others. A field that does not apply to the asset class SHOULD be null or absent rather than zero, because zero is a quantity and a denominator, not an absence. Validated by CC-13 (§5.3) as a warning.

Field notes:

  • rentable_square_feet — rentable area (RSF); the primary size field for office and industrial.
  • gross_leasable_area — GLA in square feet; the primary size field for retail.
  • net_rentable_square_feet — NRSF; the primary size field for self_storage.
  • rentable_units — the storage-unit count; a self-storage secondary.
  • keys — the room count; the primary size field for hospitality.
  • total_beds — the bed count; the primary size field for student_housing and a secondary for senior_housing.
  • gross_acres / usable_acres — the deal's own size for a land deal; usable_acres nets out what cannot be built on. Not synonyms for the existing land_area_acres, which is the parcel a building sits on — a detail field on an improved property. A land deal SHOULD state gross_acres / usable_acres and SHOULD NOT restate them as land_area_acres.
  • entitled_units — units the entitlement permits; a land secondary.
  • total_units on senior and student housing: both classes carry two counts. Senior housing sizes per unit and also states beds; student housing sizes per bed and also states units. Both figures are legitimate; Protocol §XIII.1 says which one is the denominator.

§ 4.2 — Ownership & Acquisition ​

ID: ownership
Header: ## Ownership & Acquisition {#ownership}
Purpose: Current ownership, acquisition terms, entity structure, existing debt.
Written by: wizard, agent/L2-CRE-11
Required for pipeline stage: Underwriting and beyond
Dependencies: None

json
{
  "_meta": { "...": "see §2.5" },
  "transaction_type": "acquisition | refinance | cash_out_refi | recapitalization",
  "current_owner": "string | null",
  "acquisition_date": "YYYY-MM-DD | null",
  "acquisition_price": null,
  "current_estimated_value": null,
  "existing_debt": {
    "outstanding_balance": null,
    "rate": null,
    "maturity": "YYYY-MM-DD | null",
    "lender": null,
    "prepayment_penalty_est": null
  },
  "borrowing_entity": {
    "name": "string",
    "type": "llc | lp | gp | corp | individual | trust | tic | dst | other",
    "state_of_formation": null,
    "year_formed": null,
    "ein": null
  },
  "ownership_structure_notes": null,
  "key_man_identified": false,
  "foreign_ownership_pct": null,
  "anonymous_ownership_flags": false
}

§ 4.3 — Rent Roll ​

ID: rent_roll
Header: ## Rent Roll {#rent_roll}
Purpose: Full unit-level or tenant-level schedule of leases, rents, occupancy, and concessions.
Written by: agent/L0-01, wizard:step_3 (simplified version)
Variants: multifamily (default), commercial
Required for pipeline stage: Screening and beyond
Dependencies: property

Multifamily Variant ​

json
{
  "_meta": { "...": "see §2.5" },
  "rent_roll_type": "multifamily",
  "as_of_date": "YYYY-MM-DD",
  "total_units": 0,
  "occupied_units": 0,
  "vacant_units": 0,
  "notice_units": 0,
  "model_units": 0,
  "down_units": 0,
  "physical_occupancy_pct": 0.0,
  "economic_occupancy_pct": null,
  "gross_potential_rent_monthly": 0.0,
  "gross_potential_rent_annual": 0.0,
  "in_place_rent_monthly": 0.0,
  "in_place_rent_annual": 0.0,
  "loss_to_lease_monthly": 0.0,
  "loss_to_lease_pct": 0.0,
  "concessions_monthly": 0.0,
  "concessions_annual": 0.0,
  "net_effective_rent_monthly": 0.0,
  "month_to_month_units": 0,
  "month_to_month_pct": 0.0,
  "units": [
    {
      "unit_id": "string",
      "unit_type": "string",
      "sqft": null,
      "floor": null,
      "building": null,
      "tenant_name": null,
      "lease_start": null,
      "lease_end": null,
      "lease_term_months": null,
      "monthly_rent": null,
      "market_rent": null,
      "loss_to_lease": null,
      "rent_per_sqft": null,
      "concession": null,
      "concession_monthly_equiv": null,
      "net_effective_rent": null,
      "status": "occupied | vacant | notice | model | down | admin",
      "move_in_date": null,
      "is_month_to_month": false,
      "subsidy_type": "market | section_8 | lihtc | other | null",
      "hap_contract": null,
      "notes": null
    }
  ],
  "unit_mix_summary": [
    {
      "unit_type": "string",
      "count": 0,
      "avg_sqft": null,
      "avg_rent_inplace": 0.0,
      "avg_rent_market": null,
      "loss_to_lease_avg": null,
      "occupancy_pct": 0.0,
      "pct_of_total": 0.0
    }
  ],
  "lease_expiration_schedule": [
    {
      "month": "YYYY-MM",
      "expiring_count": 0,
      "expiring_rent_monthly": 0.0,
      "pct_of_total_income": 0.0
    }
  ]
}

Commercial Variant ​

json
{
  "_meta": { "...": "see §2.5" },
  "rent_roll_type": "commercial",
  "as_of_date": "YYYY-MM-DD",
  "total_nra_sqft": 0,
  "leased_sqft": 0,
  "vacant_sqft": 0,
  "physical_occupancy_pct": 0.0,
  "weighted_avg_lease_term_years": null,
  "in_place_rent_annual": 0.0,
  "in_place_rent_per_sqft": 0.0,
  "market_rent_per_sqft": null,
  "tenants": [
    {
      "tenant_id": "string",
      "tenant_name": "string",
      "suite": null,
      "floor": null,
      "nra_sqft": 0,
      "pct_of_nra": 0.0,
      "lease_type": "nnn | gross | modified_gross | absolute_nnn | null",
      "lease_commencement": null,
      "lease_expiration": null,
      "lease_term_years": null,
      "remaining_term_years": null,
      "base_rent_annual": 0.0,
      "base_rent_per_sqft": 0.0,
      "escalation_type": "fixed_pct | cpi | fixed_dollar | none | null",
      "escalation_rate_pct": null,
      "escalation_schedule": [
        { "effective_date": "YYYY-MM-DD", "base_rent_annual": 0.0 }
      ],
      "renewal_options": [
        {
          "count": 0,
          "term_years": 0,
          "notice_months": null,
          "rent_reset": "fair_market | fixed | cpi | null"
        }
      ],
      "termination_option": {
        "earliest_date": "YYYY-MM-DD",
        "notice_months": 0,
        "penalty": null,
        "penalty_includes": ["unamortized_ti | unamortized_lc | free_rent | fee"],
        "conditions": null
      },
      "co_tenancy_clause": false,
      "co_tenancy_details": {
        "trigger": "named_tenant_departure | occupancy_threshold | both",
        "named_cotenants": [],
        "occupancy_threshold": null,
        "remedy": "rent_reduction | alternate_rent | termination_right",
        "remedy_value": null,
        "cure_period_months": null
      },
      "ti_allowance_original": null,
      "ti_outstanding_balance": null,
      "lc_original": null,
      "lc_outstanding_balance": null,
      "rofo": false,
      "rofr": false,
      "assignment_subletting": null,
      "personal_guarantee": false,
      "guarantee_amount": null,
      "guarantee_type": "personal | corporate | both | null",
      "anchor_tenant": false,
      "cam_cap_pct": null,
      "recovery_terms": {
        "method": "net | base_year_stop | fixed_stop | fixed_amount | none",
        "pro_rata_share": 0.0412,
        "share_basis": "nra | gla | stated | null",
        "base_year": null,
        "expense_stop_per_sqft": null,
        "fixed_recovery_annual": null,
        "admin_fee_pct": null,
        "gross_up_pct": null,
        "recoverable_pool": ["real_estate_taxes", "insurance", "contract_services"],
        "cap": {
          "pct": 0.05,
          "over": "base_year | prior_year",
          "accumulation": "cumulative | non_cumulative | compounding"
        }
      },
      "recovery_true_up": [
        {
          "period_start": "YYYY-MM-DD",
          "period_end": "YYYY-MM-DD",
          "pool_actual": 0.0,
          "tenant_share_uncapped": 0.0,
          "tenant_share_capped": 0.0,
          "estimated_billed": 0.0,
          "true_up_amount": 0.0,
          "settlement": "billed | credited | disputed | unsettled",
          "cash_flow_ref": null
        }
      ],
      "tenant_credit": "investment_grade | non_investment_grade | private | individual | null",
      "status": "occupied | vacant | holdover | dark | pending | null",
      "notes": null
    }
  ],
  "tenant_concentration": [
    {
      "tenant_name": "string",
      "pct_of_nra": 0.0,
      "pct_of_income": 0.0
    }
  ],
  "lease_expiration_schedule": [
    {
      "year": 0,
      "expiring_sf": 0,
      "expiring_pct_of_nra": 0.0,
      "expiring_rent_annual": 0.0
    }
  ]
}

Expense recoveries and the CAM true-up (RFC 0058). recovery_terms and recovery_true_up are OPTIONAL. A tenant stating neither is unchanged, and the legacy cam_cap_pct keeps working. Like the RFC 0055 clauses these are attributes of a lease plus a reconciliation of a closed period — not a periodic ledger, which RFC 0054 placed behind a named consumer.

Two figures are deliberately stated, not recomputed. The capped amount, because a cumulative or compounding cap depends on a base-year history no single document carries — recomputing it from one year would produce a confident wrong number, so REC-07 checks only the direction a cap can move. And the allocation of a pool across tenants, because that needs a policy for vacant space and is a modeling decision, not a recorded fact; each tenant states its own share.

  • REC-01 — pro_rata_share is a fraction in (0,1] and cap.pct a fraction in (0,1). 0.0412 is 4.12%. A share stated as a percent multiplies every recovery by a hundred, which is why this is an error and not a warning.
  • REC-02 — the method vocabulary is closed, and base_year_stop, fixed_stop and fixed_amount each REQUIRE their input (base_year, expense_stop_per_sqft, fixed_recovery_annual). A stated cap.pct REQUIRES cap.accumulation: there is no default, because cumulative, non_cumulative and compounding diverge materially inside three years.
  • REC-03 — every recoverable_pool entry names a § 4.4 operating_statement.expenses key. total_operating_expenses (a total), management_fee_pct_egi (a ratio), capital_expenditures_actual and replacement_reserves (capital, not operating) are not members. A pool naming a nonexistent expense recovers zero without saying so.
  • REC-04 — a true-up row states period_start and period_end as real dates with the end on or after the start, and any settlement from the closed vocabulary.
  • REC-05 — period_end is strictly before the rent roll's as_of_date. A reconciliation of an open period is a forecast, and this section carries settled facts. The check is anchored on as_of_date and skipped when it is absent — never on file metadata, which is an edit timestamp and would refuse a legitimately re-saved document.
  • REC-06 — tenant_share_uncapped equals pool_actual × pro_rata_share at the currency quantum, when all three are present.
  • REC-07 — tenant_share_capped does not exceed tenant_share_uncapped.
  • REC-08 — true_up_amount equals tenant_share_capped − estimated_billed at the currency quantum. A negative amount is a credit the tenant is owed.
  • REC-09 — a stated cash_flow_ref names a § 4.26 cash_flow_series variant that exists in this document. This is the cross-cutting requirement that new dated cash lands in the addressable sink, where assembly and receipt coverage already verify it. A dangling reference is the one failure that looks like success: the row claims the settled amount reached the cash flows, and nothing else checks that it did.
  • REC-10 — a warning when cam_cap_pct sits beside recovery_terms.cap. The legacy field is superseded, not removed; stating both lets them drift.

Nothing here is projected: no future recovery is forecast, no pool is grossed up from occupancy, and gross_up_pct records the occupancy a pool was grossed up to rather than performing the gross-up.

Lease clauses (RFC 0055). escalation_schedule, termination_option, co_tenancy_details, lc_original and lc_outstanding_balance are OPTIONAL. A tenant stating none of them is unchanged. These are attributes of a lease, placed here by RFC 0054 because they hold whether you read the roll in month 3 or month 40; per-period quantities belong in a period series, not on this record.

Rates are fractions, not percents. Nothing here is exercised: no rent is escalated, no break is taken, no remedy is applied and no balance is amortized.

  • LSE-01 — escalation steps are objects with a real effective_date and a finite nonnegative base_rent_annual, strictly increasing by date. A step states the resulting rent, not the increment, so a reader never compounds percentages to learn what year six costs.
  • LSE-02 — every step lies within [lease_commencement, lease_expiration] when both are stated. An unstated term checks nothing.
  • LSE-03 — a stated schedule requires escalation_type present and not none. A flat lease does not carry steps.
  • LSE-04 — termination_option.earliest_date is a real date inside the lease term, notice_months a nonnegative whole number, and penalty finite and nonnegative when stated. null means genuinely no penalty.
  • LSE-05 — penalty_includes entries come from unamortized_ti | unamortized_lc | free_rent | fee, without repeats. It records what the stated penalty is composed of; it never recomputes it.
  • LSE-06 — co_tenancy_details requires co_tenancy_clause: true, and co_tenancy_clause: true requires details. The boolean and the body agree, or neither is stated.
  • LSE-07 — the trigger carries what it needs: a nonempty named_cotenants for named_tenant_departure, an occupancy_threshold strictly between 0 and 1 for occupancy_threshold, both for both. cure_period_months is a nonnegative whole number.
  • LSE-08 — remedy_value is required for rent_reduction (a fraction in (0, 1]) and alternate_rent (nonnegative), and refused for termination_right, whose remedy is the right itself.
  • LSE-09 — TI and LC amounts are finite and nonnegative, and an outstanding balance never exceeds its original. Amortization is periodic and out of scope.

§ 4.4 — Operating Statement ​

ID: operating_statement
Header: ## Operating Statement {#operating_statement}
Purpose: Historical income and expense data from the property's financial records.
Written by: agent/L0-02
Variants: t12 (required), t3 (preferred), ytd, budget, t24
Required for pipeline stage: Full underwriting
Dependencies: property

json
{
  "_meta": { "...": "see §2.5" },
  "period_type": "T12 | T3 | YTD | Budget | T24",
  "period_start": "YYYY-MM-DD",
  "period_end": "YYYY-MM-DD",
  "annualized": true,
  "statement_prepared_by": "owner | management_company | cpa | lender | null",
  "cpa_certified": false,
  "income": {
    "gross_potential_rent": 0.0,
    "less_vacancy_credit_loss": 0.0,
    "vacancy_pct": 0.0,
    "less_concessions": 0.0,
    "less_loss_to_lease": 0.0,
    "other_income": {
      "laundry": null,
      "parking": null,
      "pet_fees": null,
      "late_fees": null,
      "storage": null,
      "utility_reimbursements": null,
      "cable_internet": null,
      "vending": null,
      "lease_termination_fees": null,
      "insurance_proceeds": null,
      "other": null,
      "total": 0.0
    },
    "effective_gross_income": 0.0
  },
  "expenses": {
    "real_estate_taxes": null,
    "insurance": null,
    "management_fees": null,
    "management_fee_pct_egi": null,
    "payroll_benefits": null,
    "utilities": {
      "electric": null,
      "gas": null,
      "water_sewer": null,
      "trash": null,
      "total": null
    },
    "repairs_maintenance": null,
    "contract_services": null,
    "marketing_advertising": null,
    "administrative": null,
    "professional_fees": null,
    "capital_expenditures_actual": null,
    "replacement_reserves": null,
    "other_expenses": null,
    "total_operating_expenses": 0.0
  },
  "net_operating_income": 0.0,
  "expense_ratio": 0.0,
  "noi_margin": 0.0,
  "noi_per_unit": null,
  "noi_per_sqft": null,
  "anomalies": [],
  "non_recurring_items": []
}

§ 4.5 — NOI Model (Underwritten) ​

ID: noi_model
Header: ## Underwritten NOI {#noi_model}
Purpose: Analyst-adjusted income and expense model representing the underwritten stabilized cash flow. All calculations here are deterministic engine outputs, not AI outputs.
Written by: system/calculations.ts (values), agent/L2-CRE-01 + agent/L2-CRE-02 (adjustments/rationale)
Required for pipeline stage: Full underwriting
Dependencies: rent_roll, operating_statement, market_analysis

json
{
  "_meta": { "...": "see §2.5" },
  "underwriting_basis": "stabilized | value_add | lease_up | as_is | stressed",
  "income": {
    "gross_potential_rent": {
      "value": 0.0,
      "source": "rent_roll | market | blended",
      "per_unit_monthly": null,
      "per_sqft_annually": null,
      "rationale": null
    },
    "vacancy_credit_loss": {
      "value": 0.0,
      "rate_applied": 0.0,
      "source": "underwritten | market | borrower_stated",
      "vs_t12_actual": null,
      "vs_submarket_avg": null,
      "rationale": null
    },
    "concessions": {
      "value": 0.0,
      "rationale": null
    },
    "loss_to_lease": {
      "value": 0.0,
      "rationale": null
    },
    "other_income": {
      "value": 0.0,
      "vs_t12": null,
      "non_recurring_excluded": null,
      "breakdown": {}
    },
    "effective_gross_income": 0.0
  },
  "expenses": {
    "real_estate_taxes": {
      "value": 0.0,
      "per_unit": null,
      "source": "actual | reassessment_estimate | benchmark",
      "sale_triggers_reassessment": false,
      "reassessment_basis": null,
      "benchmark_low": null,
      "benchmark_high": null,

      "reassessment": {
        "trigger": "sale | construction_completion | statutory_cycle | none",
        "jurisdiction": null,
        "value_basis": null,
        "assessment_ratio": null,
        "assessed_value": 0.0,
        "millage_rate": 0.0,
        "indicated_tax": 0.0,
        "round_to_decimals": null
      },
      "abatement": {
        "kind": "exemption | freeze | pilot | phase_in | credit",
        "program": null,
        "stabilized_period": null,
        "schedule": [
          { "period": "Y1", "full_tax": 0.0, "abated_tax": 0.0 }
        ]
      }
    },
    "insurance": {
      "value": 0.0,
      "pct_of_estimated_value": null,
      "source": "actual | benchmark | quote"
    },
    "management_fees": {
      "value": 0.0,
      "rate_pct": 0.0,
      "applied_to": "egi | gpr",
      "source": "contract | benchmark"
    },
    "payroll_benefits": {
      "value": 0.0,
      "per_unit": null,
      "source": "actual | benchmark"
    },
    "utilities": {
      "value": 0.0,
      "per_unit": null,
      "source": "actual | benchmark"
    },
    "repairs_maintenance": {
      "value": 0.0,
      "per_unit": null,
      "source": "actual | benchmark"
    },
    "contract_services": {
      "value": 0.0,
      "source": "actual | benchmark"
    },
    "marketing_advertising": {
      "value": 0.0,
      "source": "actual | benchmark"
    },
    "administrative": {
      "value": 0.0,
      "source": "actual | benchmark"
    },
    "professional_fees": {
      "value": 0.0,
      "source": "actual | benchmark"
    },
    "replacement_reserves": {
      "value": 0.0,
      "per_unit": null,
      "source": "actual | benchmark | replacement_cost_schedule"
    },
    "total_operating_expenses": 0.0,
    "expense_ratio": 0.0,
    "expense_per_unit": null,
    "expense_per_sqft": null,
    "vs_benchmark_assessment": "within_range | below_benchmark | above_benchmark | null"
  },
  "net_operating_income": 0.0,
  "noi_per_unit": null,
  "noi_per_sqft": null,
  "noi_margin": 0.0,
  "vs_t12_noi": null,
  "vs_t12_variance_pct": null,
  "variance_explanation": null,
  "projections": {
    "year_1": {
      "revenue_growth_rate": 0.0,
      "expense_growth_rate": 0.0,
      "projected_egi": 0.0,
      "projected_opex": 0.0,
      "projected_noi": 0.0
    },
    "year_2": {
      "projected_noi": 0.0
    }
  }
}

Property taxes and reassessment (RFC 0053). A deal carries three distinct property taxes, and this section holds the second:

TaxWhereWhose
Trailingoperating_statement.expenses.real_estate_taxesThe seller's actual.
Going-in underwrittennoi_model.expenses.real_estate_taxesThe buyer's, after their purchase triggers reassessment.
Terminaldcf.exit_analysis.terminal_tax (§4.9)The next buyer's, after this sale reassesses at the exit price.

reassessment and abatement are OPTIONAL. A document omitting both is unchanged. Rates are fractions, not percents. The prose reassessment_basis is retained as narrative; when reassessment is present it governs.

  • TAX-01 — trigger is one of the four listed values, assessed_value, millage_rate and indicated_tax are finite, and when value_basis and assessment_ratio are both stated, assessed_value MUST equal their product. Stating one of that pair without the other is refused.
  • TAX-02 — indicated_tax MUST equal assessed_value × millage_rate.
  • TAX-03 — value MUST equal indicated_tax quantized at round_to_decimals, which MUST be an integer and MAY be negative to round to a magnitude. Omitting it means the §VIII.9.4 currency quantum, so an unrounded value MUST foot exactly.
  • TAX-04 — sale_triggers_reassessment, when stated as a boolean, MUST agree with trigger === "sale".
  • TAX-05 — abatement schedule periods are RFC 0041 selectors (Y1+, YYYY-Qn, YYYY-MM, or YYYY-MM-DD), all of one granularity, strictly increasing, without duplicates. Holding years order numerically, so Y10 follows Y9.
  • TAX-06 — kind is one of the five listed values and every entry holds 0 ≤ abated_tax ≤ full_tax with both finite. A freeze additionally holds full_tax nondecreasing.
  • TAX-07 — when stabilized_period is stated it MUST name a period in the schedule, and value MUST equal that period's abated_tax.

The schedule documents and verifies the stated tax line; it does not project it. No per-period expense rows are generated, and no jurisdiction rules are inferred from jurisdiction, which is a label.


§ 4.6 — Valuation ​

ID: valuation
Header: ## Valuation {#valuation}
Purpose: Value conclusions using income approach, with third-party appraisal cross-check.
Written by: agent/L2-CRE-04, system/calculations.ts
Required for pipeline stage: Full underwriting
Dependencies: noi_model, market_analysis

json
{
  "_meta": { "...": "see §2.5" },
  "purchase_price": null,
  "purchase_price_per_unit": null,
  "purchase_price_per_sqft": null,
  "gross_rent_multiplier": null,
  "income_approach": {
    "cap_rate_applied": 0.0,
    "cap_rate_source": "market_comparable | appraiser | underwriter | investor_target | blended",
    "cap_rate_range": { "low": null, "mid": 0.0, "high": null },
    "noi_used": 0.0,
    "noi_source": "underwritten | t12 | annualized_t3",
    "indicated_value": 0.0,
    "value_per_unit": null,
    "value_per_sqft": null
  },
  "appraised_value": null,
  "appraised_as_of": null,
  "appraiser_name": null,
  "appraisal_report_date": null,
  "appraisal_approach": "income | sales_comparison | cost | blended | null",
  "appraisal_cap_rate": null,
  "sales_comparison": {
    "indicated_value": null,
    "price_per_unit_range_low": null,
    "price_per_unit_range_high": null,
    "comp_count": null
  },
  "uw_value_vs_purchase_price_pct": null,
  "uw_value_vs_appraised_pct": null,
  "appraisal_inflation_flag": false,
  "value_used_for_ltv": 0.0,
  "value_used_for_ltv_basis": "appraised | underwritten | lesser_of | purchase_price"
}

§ 4.7 — Debt Structure ​

ID: debt_structure
Header: ## Debt Structure {#debt_structure}
Purpose: Complete loan terms, sizing metrics, covenants, and rate sensitivity.
Written by: wizard:step_4, agent/L4-01, agent/L4-04
Required for pipeline stage: Screening and beyond
Dependencies: valuation, noi_model

json
{
  "_meta": { "...": "see §2.5" },
  "loan_amount": 0.0,
  "loan_purpose": "acquisition | refinance | cash_out_refi | construction | bridge | supplement",
  "loan_type": "conventional | agency_fannie | agency_freddie | cmbs | sba_504 | sba_7a | bridge | construction | life_co | hud_221d4 | hud_223f | mezzanine | null",
  "rate_type": "fixed | floating | hybrid",
  "interest_rate": 0.0,
  "rate_index": "sofr | prime | treasury_5yr | treasury_10yr | fixed | null",
  "rate_spread_bps": null,
  "rate_floor_pct": null,
  "rate_cap_pct": null,
  "rate_hedge": null,
  "note_rate_at_close": null,
  "amortization_years": 30,
  "loan_term_years": 0,
  "io_period_months": 0,
  "balloon_year": null,
  "balloon_payment_est": null,
  "recourse": "full | partial | non_recourse | carve_outs_only",
  "prepayment_type": "stepdown | yield_maintenance | defeasance | open | lockout | none | null",
  "prepayment_schedule": null,
  "origination_fee_pct": null,
  "exit_fee_pct": null,
  "lender_name": null,
  "lender_type": "bank | credit_union | cmbs_conduit | agency | life_co | debt_fund | private | sba | null",
  "annual_debt_service": 0.0,
  "monthly_debt_service": 0.0,
  "sizing_metrics": {
    "ltv": 0.0,
    "dscr_underwritten": 0.0,
    "dscr_inplace": null,
    "debt_yield": 0.0,
    "debt_yield_inplace": null,
    "binding_constraint": "ltv | dscr | debt_yield | null",
    "max_loan_at_ltv": null,
    "max_loan_at_dscr": null,
    "max_loan_at_debt_yield": null,
    "headroom_ltv_bps": null,
    "headroom_dscr": null,
    "headroom_debt_yield_bps": null
  },
  "stress_dscr": {
    "plus_100bps": null,
    "plus_200bps": null,
    "plus_300bps": null
  },
  "covenants": [
    {
      "type": "min_dscr | max_ltv | min_occupancy | min_nw_guarantor | min_liquidity | cash_trap | restricted_payments | leasing_threshold",
      "threshold": "string",
      "test_frequency": "annual | quarterly | monthly | at_maturity",
      "cure_period_days": null,
      "remedy_cascade": null
    }
  ]
}

Rate hedges (RFC 0056). rate_hedge is OPTIONAL and types what rate_cap_pct only gestures at. A loan stating neither is unchanged.

json
"rate_hedge": {
  "instrument": "rate_cap",
  "notional": 30000000.0,
  "strike_rate": 0.035,
  "index": "sofr | prime | treasury_5yr | treasury_10yr",
  "effective_date": "2026-01-01",
  "expiration_date": "2029-01-01",
  "premium": 410000.0,
  "post_expiration_assumption": "replace | unhedged | loan_matures_first",
  "counterparty": null
}

Rates are fractions, not percents. Nothing here is priced: the premium is the cash the author paid, not a value struck off a forward curve, and no strike crossing is projected.

  • HDG-01 — instrument is from the closed set; notional finite and nonnegative; strike_rate a fraction strictly between 0 and 1; index from the closed set (the rate_index vocabulary minus fixed, which a cap is not struck against); both dates real, with expiration_date strictly after effective_date; premium finite and nonnegative when stated, null for genuinely none.
  • HDG-02 — rate_swap and rate_collar are reserved and refused. Their mark-to-market moves with the curve and can be negative, which a cap's cannot; typing them as caps would make the capital stack wrong in the one case that matters. They wait for their own contract.
  • HDG-03 — a stated rate_hedge requires rate_type to be floating or hybrid. A fixed-rate loan does not carry a rate cap.
  • HDG-04 — strike_rate agrees with the legacy rate_cap_pct when both are stated.
  • HDG-05 — premium agrees with sources_uses.uses.rate_cap_cost at the currency quantum (protocol § VIII.5) when both are stated. It is the same cash seen from the debt side and from the use that funds it.
  • HDG-06 — post_expiration_assumption is stated and from the closed set. It has no default on purpose: a cap's term is the fact the reader came for, and leaving it unstated is the answer that hides the cliff. replace is tied to a funded escrow line by ESC-04 (§ 4.8).

notional is the member that makes partial hedging legible. A $40M loan with a $30M notional is 75% hedged; without it, that deal and a fully hedged one read identically.


§ 4.8 — Sources & Uses ​

ID: sources_uses
Header: ## Sources & Uses {#sources_uses}
Purpose: Complete capital stack — all equity, debt, and cost components.
Written by: wizard:step_4, agent/L4-05
Required for pipeline stage: Full underwriting
Dependencies: debt_structure, valuation

json
{
  "_meta": { "...": "see §2.5" },
  "total_project_cost": 0.0,
  "sources": {
    "senior_loan": 0.0,
    "mezzanine_debt": null,
    "preferred_equity": null,
    "equity_sponsor": 0.0,
    "equity_lp": null,
    "seller_financing": null,
    "government_grant": null,
    "tax_credit_equity": null,
    "other": null,
    "total": 0.0
  },
  "uses": {
    "purchase_price": 0.0,
    "closing_costs": {
      "broker_commission": null,
      "title_insurance": null,
      "transfer_taxes": null,
      "legal_fees": null,
      "due_diligence": null,
      "loan_origination_fee": null,
      "appraisal": null,
      "environmental": null,
      "survey": null,
      "inspection": null,
      "other": null,
      "total": 0.0
    },
    "renovation_budget": null,
    "renovation_contingency": null,
    "renovation": null,
    "operating_reserves": null,
    "interest_reserve": null,
    "rate_cap_cost": null,
    "other_reserves": null,
    "escrows": null,
    "total": 0.0
  },
  "equity_metrics": {
    "equity_total": 0.0,
    "equity_pct_of_cost": 0.0,
    "equity_per_unit": null,
    "equity_per_sqft": null,
    "loan_to_cost": null
  },
  "sources_uses_balanced": true
}

Escrows (RFC 0056). uses.escrows is OPTIONAL and types the cash lines the four flat scalars above cannot describe: which are lender-required, which fund monthly rather than at close, and that the tax and insurance escrows exist at all. A document stating none of them is unchanged.

json
"escrows": [
  {
    "name": "tax | insurance | replacement_reserve | ti_lc | interest | operating | rate_cap_replacement | other",
    "label": null,
    "upfront": 120000.0,
    "monthly": 30000.0,
    "lender_required": true
  }
]

The closed vocabulary with a label-bearing other is RFC 0052's shape, for the same reason: an open string makes two documents incomparable, and a closed list with no escape hatch makes the honest author lie. Nothing is rolled forward — an amortizing balance is periodic, which RFC 0054 placed behind a named consumer.

  • ESC-01 — escrows is a nonempty array of objects; name from the closed set; upfront and monthly finite and nonnegative when stated, with at least one of the two stated. An escrow that funds neither at close nor monthly is not one.
  • ESC-02 — other requires a nonempty label and every other name refuses one, since it names itself. name is unique across the array, and other entries are distinguished by unique labels.
  • ESC-03 — uses.interest_reserve and uses.operating_reserves agree with the interest and operating escrows' upfront at the currency quantum (protocol § VIII.5) when both are stated.
  • ESC-04 — debt_structure.rate_hedge.post_expiration_assumption: "replace" requires a rate_cap_replacement escrow, and a rate_cap_replacement escrow requires that assumption. This is the rule that turns "the cap expires in year three" from a note into a funded line; a replacement bought into a higher-rate environment is routinely the larger of the two premiums.

The renovation draw (RFC 0057). uses.renovation is OPTIONAL and types what renovation_budget and renovation_contingency leave unsaid: whether either was spent. Two deals stating the same contingency are indistinguishable when one has drawn none of it and the other has drawn all of it.

json
"renovation": {
  "budget": 4000000.0,
  "contingency": 500000.0,
  "contingency_used": 120000.0,
  "contingency_remaining": 380000.0,
  "drawn_to_date": 1850000.0,
  "as_of_date": "2026-09-01",
  "expense_targeted": []
}

A draw is a fact as of as_of_date. Nothing is projected: there is no S-curve, no remaining-to-spend schedule and no milestone release. A draw over time is periodic, which RFC 0054 placed behind a named consumer.

  • CAPX-01 — budget, contingency, contingency_used and drawn_to_date are finite and nonnegative, and as_of_date is a real YYYY-MM-DD date.
  • CAPX-02 — contingency_used does not exceed contingency. A contingency drawn past its size is an overrun, and calling it a contingency is what hides that.
  • CAPX-03 — drawn_to_date does not exceed budget + contingency, and is at least contingency_used: the contingency draw is part of the total, not beside it.
  • CAPX-04 — contingency_remaining, when stated, equals contingency − contingency_used at the currency quantum (protocol § VIII.5). It is stated and verified rather than left to the reader's subtraction, the posture RFC 0052 took with net_sale_proceeds.
  • CAPX-05 — budget and contingency agree with the legacy uses.renovation_budget and uses.renovation_contingency at the currency quantum when both are stated.

Expense-targeted capex (RFC 0057). renovation.expense_targeted is OPTIONAL and names capital spend bought specifically to reduce an operating expense. A $600,000 retrofit that takes $95,000 a year out of the utility line is a different thing from $600,000 of deferred maintenance, and the format had no way to say which one it was looking at.

json
{
  "label": "LED and controls retrofit",
  "amount": 600000.0,
  "targets": "utilities",
  "annual_savings": 95000.0,
  "savings_begin": "Y2",
  "in_noi_model": false,
  "simple_payback_years": 6.3158
}

The saving is stated, never applied. Nothing subtracts annual_savings from an expense line, from EGI or from NOI — invariant 1 holds here as everywhere, and this is the surface most likely to tempt someone into breaking it.

  • CAPX-06 — label nonempty; amount and annual_savings finite and nonnegative; savings_begin an RFC 0041 period selector; targets names a key that exists under noi_model.expenses when that section is present. The target is checked against the keys actually there, not a closed list, so a module adding a class-specific expense line keeps working.
  • CAPX-07 — in_noi_model is stated as a boolean, saying whether the author has already reflected this saving in noi_model. It is required because a stated saving with no such flag is how a document gets double-counted: the author reduces the line, and a second reader applies the saving again.
  • CAPX-08 — simple_payback_years, when stated, equals amount ÷ annual_savings at four decimals, and is refused against zero savings. A project with no stated saving has no payback period, and a number there would be a fiction.

Redevelopment downtime needs no new field: § 4.25 lease_up_schedule with model_type: "natural_turnover" already expresses a period of suppressed occupancy carrying its own ti_lc_capex, which is what downtime is.


§ 4.9 — DCF & Hold Period Analysis ​

ID: dcf
Header: ## DCF & Hold Period {#dcf}
Purpose: Multi-year discounted cash flow model, exit analysis, and returns.
Written by: system/dcfEngine.ts
Required for pipeline stage: Full underwriting
Dependencies: noi_model, debt_structure

json
{
  "_meta": { "...": "see §2.5" },
  "hold_period_years": 5,
  "analysis_start_date": null,
  "assumptions": {
    "revenue_growth_rate": 0.0,
    "expense_growth_rate": 0.0,
    "exit_cap_rate": 0.0,
    "exit_cap_rate_source": "string",
    "exit_cap_spread_over_going_in_bps": null,
    "disposition_costs_pct": 0.02,
    "discount_rate": null,
    "going_in_cap_rate": null
  },
  "annual_cash_flows": [
    {
      "year": 1,
      "gross_potential_rent": 0.0,
      "effective_gross_income": 0.0,
      "total_expenses": 0.0,
      "net_operating_income": 0.0,
      "annual_debt_service": 0.0,
      "net_cash_flow_levered": 0.0,
      "net_cash_flow_unlevered": 0.0,
      "cash_on_cash_return": 0.0,
      "cumulative_equity_invested": 0.0,
      "loan_balance_eoy": null
    }
  ],
  "exit_analysis": {
    "exit_year": 5,
    "exit_noi": 0.0,
    "exit_cap_rate": 0.0,
    "exit_value_gross": 0.0,
    "disposition_costs": 0.0,
    "exit_value_net": 0.0,
    "loan_balance_at_exit": 0.0,
    "net_proceeds_to_equity": 0.0,
    "exit_value_per_unit": null,
    "exit_value_per_sqft": null,
    "terminal_tax": {
      "trigger": "sale | construction_completion | statutory_cycle | none",
      "jurisdiction": null,
      "value_basis": null,
      "assessment_ratio": null,
      "assessed_value": 0.0,
      "millage_rate": 0.0,
      "indicated_tax": 0.0,
      "round_to_decimals": null,
      "in_exit_noi": false,
      "value_basis_differs_because": null
    }
  },
  "returns": {
    "tax_basis": "pre_tax",
    "levered_irr": null,
    "unlevered_irr": null,
    "equity_multiple": null,
    "avg_cash_on_cash": null,
    "total_equity_distributions": null,
    "npv": null,
    "discount_rate_used": null,
    "payback_period_years": null
  },
  "sensitivity_matrix": {
    "exit_cap_rate_axis": [],
    "rent_growth_axis": [],
    "irr_grid": []
  }
}

returns.tax_basis (RFC 0038). Declares the tax basis of every metric stated in returns — levered_irr, unlevered_irr, equity_multiple, avg_cash_on_cash, total_equity_distributions, npv, payback_period_years: pre_tax (the default when the field is absent) or after_tax. The declaration is per section, not per metric — a returns object MUST NOT mix bases; a producer that has both states one and carries the other in an extension or a separate document. Any other value is RT-01 (error, Protocol §III.6a). Readers that compare return metrics across documents MUST treat differing bases as incomparable rather than as a spread. frontmatter.quick_metrics.irr_projected inherits the dcf declaration. getReturnTaxBasis() in @uwmd/core returns the effective basis, default included.

Terminal tax (RFC 0053). exit_analysis.terminal_tax is OPTIONAL and holds the next buyer's tax, after this sale reassesses the property at the exit price. It takes the §4.5 reassessment shape and the same TAX-01 through TAX-03 rules, plus:

  • TAX-08 — in_exit_noi MUST be stated as a boolean, recording whether this tax is already inside exit_noi. When trigger is sale, value_basis MUST equal exit_analysis.exit_value_gross — the next buyer is reassessed at what they pay — unless value_basis_differs_because gives a nonempty reason.

TAX-08 is what catches a going-in tax carried into terminal NOI, which overstates exit value.

The engine does not solve the circularity. Exit value depends on the reassessed tax, which depends on exit value. There is no iteration in the calc engine and this section adds none: an author who wants the converged figure runs that solve in their own model and states the result, which these rules then check for internal consistency. No arithmetic is asserted over exit_noi itself, because exit_value_gross capitalizes a forward (year N+1) NOI this block does not store — the same reason deriveDCF leaves it an input.


§ 4.10 — Stress Tests ​

ID: stress_tests
Header: ## Stress Tests {#stress_tests}
Purpose: Scenario analysis showing deal performance under adverse conditions.
Written by: system/calculations.ts, agent/L2-CRE-12
Required for pipeline stage: Credit approval
Dependencies: noi_model, debt_structure

json
{
  "_meta": { "...": "see §2.5" },
  "base_dscr": 0.0,
  "base_debt_yield": 0.0,
  "base_noi": 0.0,
  "scenarios": [
    {
      "name": "base_case | mild_stress | moderate_stress | severe_stress | custom",
      "label": "string",
      "revenue_shock_pct": 0.0,
      "expense_shock_pct": 0.0,
      "rate_shock_bps": 0,
      "cap_rate_expansion_bps": null,
      "resulting_egi": 0.0,
      "resulting_opex": 0.0,
      "resulting_noi": 0.0,
      "resulting_debt_service": 0.0,
      "resulting_dscr": 0.0,
      "resulting_debt_yield": 0.0,
      "resulting_ltv": null,
      "resulting_exit_value": null,
      "passes_dscr_minimum": true,
      "minimum_dscr_threshold": 1.0,
      "passes_debt_yield_minimum": true,
      "minimum_debt_yield_threshold": null
    }
  ],
  "break_even": {
    "break_even_occupancy_pct": 0.0,
    "break_even_rent_per_unit_monthly": null,
    "break_even_rent_per_sqft_annually": null,
    "current_vs_breakeven_occupancy_cushion": 0.0,
    "current_vs_breakeven_rent_cushion_pct": null
  },
  "rate_sensitivity": [
    {
      "rate_change_bps": 0,
      "resulting_rate_pct": 0.0,
      "resulting_annual_debt_service": 0.0,
      "resulting_dscr": 0.0
    }
  ]
}

§ 4.11 — Market Analysis ​

ID: market_analysis
Header: ## Market Analysis {#market_analysis}
Purpose: Submarket conditions, rent comparables, sales comparables, and demand outlook.
Written by: agent/L2-CRE-07, agent/L2-CRE-08
Required for pipeline stage: Full underwriting
Dependencies: property

json
{
  "_meta": { "...": "see §2.5" },
  "market": "string",
  "submarket": "string",
  "market_tier": "1 | 2 | 3 | null",
  "data_as_of": "YYYY-MM-DD | null",
  "data_sources": [],
  "vacancy": {
    "current_rate": null,
    "prior_year_rate": null,
    "yoy_change_bps": null,
    "trend": "improving | stable | deteriorating | null",
    "source": null
  },
  "rents": {
    "avg_asking_per_unit": null,
    "avg_asking_per_sqft": null,
    "yoy_rent_growth_pct": null,
    "trend": "strong | moderate | flat | declining | null",
    "by_unit_type": []
  },
  "supply": {
    "units_under_construction": null,
    "units_planned_12mo": null,
    "units_planned_24mo": null,
    "pct_of_existing_stock": null,
    "major_projects": [],
    "supply_assessment": "low | moderate | elevated | high | null"
  },
  "demand": {
    "net_absorption_trailing_12mo": null,
    "employment_growth_pct": null,
    "population_growth_pct": null,
    "major_employers": [],
    "demand_drivers": [],
    "outlook": "positive | neutral | negative | null"
  },
  "cap_rates": {
    "range_low": null,
    "range_mid": null,
    "range_high": null,
    "trend": "compressing | stable | expanding | null",
    "source": null
  },
  "comparable_sales": [
    {
      "address": null,
      "property_name": null,
      "sale_date": null,
      "units_or_sqft": null,
      "year_built": null,
      "sale_price": null,
      "price_per_unit": null,
      "price_per_sqft": null,
      "cap_rate": null,
      "distance_miles": null,
      "notes": null
    }
  ],
  "comparable_rentals": [
    {
      "property_name": null,
      "address": null,
      "units": null,
      "unit_type": null,
      "asking_rent_per_unit": null,
      "asking_rent_per_sqft": null,
      "occupancy_pct": null,
      "year_built": null,
      "distance_miles": null,
      "concessions": null
    }
  ],
  "market_risk_rating": "low | moderate | elevated | high | null",
  "market_narrative": "string | null"
}

§ 4.12 — Borrower & Sponsor ​

ID: borrower_sponsor
Header: ## Borrower & Sponsor {#borrower_sponsor}
Purpose: Borrowing entity, principals, financial strength, and CRE track record.
Written by: wizard (form), agent/L2-CRE-09, agent/L2-CRE-10, agent/L2-CRE-11
Required for pipeline stage: Full underwriting
Dependencies: ownership

json
{
  "_meta": { "...": "see §2.5" },
  "principals": [
    {
      "name": "string",
      "role": "managing_member | gp | owner | guarantor | key_principal | investor | other",
      "ownership_pct": null,
      "is_guarantor": false,
      "is_key_man": false,
      "net_worth_stated": null,
      "liquid_assets_stated": null,
      "contingent_liabilities_stated": null,
      "years_cre_experience": null,
      "pfs_received": false,
      "tax_returns_received": false,
      "figures_verified": false,
      "verification_basis": "tax_returns | cpa_letter | pfs_stated | bank_statements | null"
    }
  ],
  "financial_summary": {
    "global_net_worth": null,
    "global_liquidity": null,
    "nw_to_loan_ratio": null,
    "nw_to_loan_policy_min": null,
    "liquidity_to_loan_ratio": null,
    "liquidity_to_loan_policy_min": null,
    "global_debt_service_annual": null,
    "global_dscr": null,
    "all_figures_verified": false,
    "unverified_flag": true
  },
  "real_estate_portfolio": {
    "total_properties": null,
    "total_value_stated": null,
    "total_debt_stated": null,
    "implied_equity": null,
    "portfolio_ltv": null,
    "asset_classes_managed": [],
    "markets_active_in": []
  },
  "track_record": {
    "deals_completed": null,
    "total_deal_volume": null,
    "avg_hold_period_years": null,
    "similar_asset_class_experience": null,
    "similar_market_experience": null,
    "deals_in_default_or_loss": null,
    "summary_narrative": null
  },
  "entity_structure_flags": [],
  "key_man_risk_flag": false,
  "sponsor_narrative": null
}

§ 4.13 — Due Diligence ​

ID: due_diligence
Header: ## Due Diligence {#due_diligence}
Purpose: Third-party report status, findings, and clearance for all DD items.
Written by: agent/L3-01 through agent/L3-06
Required for pipeline stage: Credit approval and closing
Dependencies: property

json
{
  "_meta": { "...": "see §2.5" },
  "appraisal": {
    "received": false,
    "appraiser_name": null,
    "appraiser_cert_no": null,
    "firm": null,
    "on_approved_vendor_list": null,
    "effective_date": null,
    "report_date": null,
    "as_is_value": null,
    "as_stabilized_value": null,
    "as_complete_value": null,
    "cap_rate_applied": null,
    "income_approach_noi": null,
    "sales_comp_indicated_value": null,
    "extraordinary_assumptions": [],
    "hypothetical_conditions": [],
    "exposure_time_months": null,
    "marketing_time_months": null,
    "uspap_compliant": null,
    "within_policy_age": null,
    "policy_max_age_months": null,
    "adequacy_rating": "acceptable | acceptable_with_conditions | requires_revision | not_reviewed | null",
    "flags": []
  },
  "environmental": {
    "phase1_received": false,
    "phase2_received": false,
    "consultant_name": null,
    "firm": null,
    "phase1_report_date": null,
    "phase2_report_date": null,
    "rec_count": null,
    "rec_descriptions": [],
    "hrec_count": null,
    "crec_count": null,
    "vec_flag": false,
    "regulatory_database_hits": null,
    "phase2_recommended": null,
    "phase2_reason": null,
    "remediation_required": false,
    "remediation_cost_est": null,
    "clearance_status": "clear | recs_present | phase2_required | remediation_required | not_reviewed | null",
    "flags": []
  },
  "title": {
    "commitment_received": false,
    "effective_date": null,
    "title_company": null,
    "commitment_amount": null,
    "existing_liens": [],
    "schedule_b_exceptions": [],
    "material_exceptions": [],
    "required_endorsements": [],
    "property_tax_current": null,
    "mechanic_liens_present": false,
    "judgment_liens_present": false,
    "deed_restrictions": [],
    "easements": [],
    "clearance_status": "clear | exceptions_to_clear | material_issues | not_reviewed | null"
  },
  "survey": {
    "received": false,
    "survey_date": null,
    "surveyor": null,
    "survey_type": "alta | boundary | topographic | null",
    "encroachments_found": null,
    "easements_noted": [],
    "setback_issues": null,
    "parking_count_confirmed": null,
    "lot_size_sqft_confirmed": null,
    "clearance_status": "clear | issues_noted | not_reviewed | null"
  },
  "inspection": {
    "received": false,
    "inspector": null,
    "firm": null,
    "inspection_date": null,
    "building_systems_assessed": [],
    "deferred_maintenance_est": null,
    "immediate_needs": [],
    "short_term_needs_0_3yr": [],
    "long_term_needs_3_10yr": [],
    "major_system_remaining_life": {
      "roof_years": null,
      "hvac_years": null,
      "plumbing_years": null,
      "electrical_years": null,
      "elevators_years": null
    },
    "clearance_status": "clear | items_noted | significant_issues | not_reviewed | null"
  },
  "seismic": {
    "required": false,
    "received": false,
    "pml_pct": null,
    "clearance_status": "clear | pml_exceeds_threshold | not_reviewed | null"
  },
  "checklist": {
    "total_items_required": 0,
    "items_received": 0,
    "items_outstanding": [],
    "blocking_items": [],
    "ready_to_close": false
  }
}

§ 4.14 — Risk Assessment ​

ID: risk_assessment
Header: ## Risk Assessment {#risk_assessment}
Purpose: Component risk scores, composite rating, and credit recommendation.
Written by: agent/L6-01 through agent/L6-06
Required for pipeline stage: Credit approval
Dependencies: noi_model, valuation, market_analysis, borrower_sponsor, debt_structure

json
{
  "_meta": { "...": "see §2.5" },
  "scoring_model_version": "string",
  "component_scores": {
    "property": {
      "score": null,
      "max_score": 10,
      "weight": 0.25,
      "sub_factors": {
        "asset_quality": null,
        "physical_condition": null,
        "lease_structure": null,
        "functional_obsolescence": null,
        "location_quality": null
      },
      "strengths": [],
      "weaknesses": [],
      "rationale": null
    },
    "market": {
      "score": null,
      "max_score": 10,
      "weight": 0.20,
      "sub_factors": {
        "vacancy_trend": null,
        "supply_pipeline": null,
        "demand_outlook": null,
        "market_liquidity": null,
        "cap_rate_trend": null
      },
      "strengths": [],
      "weaknesses": [],
      "rationale": null
    },
    "borrower": {
      "score": null,
      "max_score": 10,
      "weight": 0.25,
      "sub_factors": {
        "financial_strength": null,
        "experience": null,
        "track_record": null,
        "management_depth": null
      },
      "strengths": [],
      "weaknesses": [],
      "rationale": null
    },
    "loan_structure": {
      "score": null,
      "max_score": 10,
      "weight": 0.30,
      "sub_factors": {
        "ltv": null,
        "dscr": null,
        "debt_yield": null,
        "covenant_package": null,
        "recourse_structure": null,
        "io_period_risk": null
      },
      "strengths": [],
      "weaknesses": [],
      "rationale": null
    }
  },
  "composite_score": null,
  "composite_rating_1_to_10": null,
  "regulatory_rating": "pass | special_mention | substandard | doubtful | loss | null",
  "top_risks": [],
  "top_mitigants": [],
  "risk_narrative": null,
  "recommendation": "approve | approve_with_conditions | decline | pending_human_review | null",
  "conditions": [],
  "decline_reasons": [],
  "policy_exceptions_required": [],
  "approval_authority_required": null,
  "status": "draft | preliminary | final",
  "human_review_required": true
}

§ 4.15 — Compliance ​

ID: compliance
Header: ## Compliance {#compliance}
Purpose: Regulatory classification, AML screening, fair lending, and concentration checks.
Written by: agent/L5-01 through agent/L5-06
Required for pipeline stage: Credit approval
Dependencies: debt_structure, ownership, borrower_sponsor

json
{
  "_meta": { "...": "see §2.5" },
  "hvcre": {
    "classification": "hvcre | non_hvcre | uncertain | not_applicable | null",
    "contributing_capital_test_met": null,
    "ltv_within_regulatory_limits": null,
    "pre_sold_leased_pct": null,
    "one_to_four_family_exclusion": false,
    "risk_weight_pct": null,
    "regulatory_citation": null,
    "rationale": null
  },
  "cra": {
    "eligible": null,
    "census_tract": null,
    "census_tract_income_category": "low | moderate | middle | upper | null",
    "majority_minority_tract": null,
    "lmi_census_tract": null,
    "small_business_loan_eligible": null,
    "cra_category": null
  },
  "bsa_aml": {
    "ofac_sdn_clear": null,
    "fincen_314a_clear": null,
    "pep_flags": [],
    "anonymous_structure_flag": false,
    "foreign_ownership_flag": false,
    "high_risk_geography_flag": false,
    "structuring_indicators": [],
    "overall_bsa_risk": "low | medium | high | null",
    "escalation_required": false,
    "escalation_reason": null
  },
  "fair_lending": {
    "review_triggered": false,
    "trigger_reasons": [],
    "pricing_variance_flag": false,
    "appraisal_bias_flag": false,
    "compliance_officer_review_required": false
  },
  "concentration": {
    "cre_total_pct_of_capital": null,
    "construction_pct_of_capital": null,
    "single_borrower_pct_of_tier1": null,
    "geographic_concentration_flag": false,
    "asset_class_concentration_flag": false,
    "within_all_concentration_limits": null,
    "nearest_limit_headroom_pct": null
  },
  "policy_exceptions": [
    {
      "exception_type": "string",
      "policy_code": "string",
      "description": "string",
      "value_requested": "string",
      "policy_limit": "string",
      "justification": null,
      "approval_authority": null,
      "status": "pending | approved | denied | null"
    }
  ]
}

§ 4.16 — Assumptions Registry ​

ID: assumptions
Header: ## Assumptions {#assumptions}
Purpose: Complete audit trail of every assumption used in the underwriting, with source and provenance. This is the single source of truth for where every number came from.
Written by: system/calculations.ts (compiles from all sections)
Required for pipeline stage: Full underwriting
Dependencies: All financial sections

json
{
  "_meta": { "...": "see §2.5" },
  "assumptions": [
    {
      "key": "string",
      "label": "string",
      "section": "string",
      "category": "income | expense | financing | market | exit | scenario | borrower",
      "value": null,
      "unit": "pct | dollar | dollar_per_unit | dollar_per_sqft | years | months | bps | ratio | count | boolean | string",
      "source": "user_input | user_override | manual | inherited_assumption | investor_profile | market_data | market_data_accepted | ai_extracted | agent_computed | asset_class_default | scenario_default | global_default | system_default",
      "source_detail": null,
      "agent": null,
      "timestamp": "ISO8601",
      "confidence": "high | medium | low",
      "is_overridden": false,
      "original_value": null,
      "override_rationale": null,
      "linked_to_policy": false,
      "policy_threshold": null
    }
  ],
  "summary": {
    "total_assumptions": 0,
    "by_source": {
      "user_input": 0,
      "user_override": 0,
      "market_data": 0,
      "ai_extracted": 0,
      "agent_computed": 0,
      "investor_profile": 0,
      "asset_class_default": 0,
      "scenario_default": 0
    },
    "low_confidence_count": 0,
    "override_count": 0
  }
}

An assumption entry's source takes one canonical tag from SOURCE_TAGS (§2.6 resolution vocabulary — this registry aggregates by method, which is the question "how much of this underwriting is assumed?" needs answered). wizard_input, which this section used to require and no other vocabulary or line of code recognized, is user_input (RFC 0031). summary.by_source keys follow the same vocabulary; the keys shown above are illustrative, not exhaustive.


§ 4.17 — Flags & Validation ​

ID: validation
Header: ## Flags & Validation {#validation}
Purpose: All warnings, errors, policy flags, and cross-section consistency checks. This section is the machine-readable deal health signal.
Written by: system/financialValidityChecker
Required for pipeline stage: All stages
Dependencies: All sections (reads everything)

json
{
  "_meta": { "...": "see §2.5" },
  "overall_status": "clean | warnings_only | errors_present | blocking",
  "financial_validity": [
    {
      "flag_id": "string",
      "metric": "dscr | ltv | cap_rate | vacancy_rate | opex_ratio | irr | rent_growth | equity_multiple | debt_yield | noi | other",
      "value": null,
      "threshold": {
        "type": "min | max | range",
        "min": null,
        "max": null
      },
      "severity": "error | warning | info",
      "message": "string",
      "suppressed": false,
      "suppress_reason": null
    }
  ],
  "completeness": [
    {
      "section": "string",
      "field": "string",
      "required_for_stage": "screening | underwriting | credit_approval | closing",
      "severity": "blocking | warning | info",
      "message": "string"
    }
  ],
  "cross_section_consistency": [
    {
      "flag_id": "string",
      "sections_involved": [],
      "description": "string",
      "severity": "error | warning | info",
      "field_a": { "section": "string", "field": "string", "value": null },
      "field_b": { "section": "string", "field": "string", "value": null },
      "variance": null
    }
  ],
  "policy_flags": [
    {
      "policy_code": "string",
      "description": "string",
      "deal_value": "string",
      "policy_threshold": "string",
      "exception_filed": false,
      "exception_approved": false
    }
  ],
  "human_review_items": [],
  "thresholds_used": {
    "dscr_error_below": 1.0,
    "dscr_warning_below": 1.20,
    "ltv_warning_above": 0.75,
    "ltv_error_above": 0.85,
    "cap_rate_warning_below": 0.03,
    "cap_rate_warning_above": 0.15,
    "vacancy_warning_below": 0.02,
    "vacancy_warning_above": 0.40,
    "opex_ratio_warning_below": 0.20,
    "opex_ratio_warning_above": 0.70,
    "irr_warning_below": 0.05,
    "irr_warning_above": 0.40,
    "annual_rent_growth_warning_above": 0.08,
    "equity_multiple_warning_below": 1.0,
    "equity_multiple_warning_above": 5.0
  }
}

§ 4.18 — Pipeline Log ​

ID: pipeline_log
Header: ## Pipeline Log {#pipeline_log}
Purpose: Immutable append-only execution history. Every agent run, user edit, render, and validation event is recorded. This is the audit trail.
Written by: All tools (append-only — never superseded, only appended to)
Required for pipeline stage: All stages

json
{
  "_meta": { "...": "see §2.5 (note: pipeline_log _meta has no superseded field — it is always append-only)" },
  "entries": [
    {
      "entry_id": "string",
      "timestamp": "ISO8601",
      "event_type": "file_created | agent_run | user_edit | wizard_input | engine_run | render | import | export | validation | compact | note | flag_raised | flag_cleared | human_review | approval",
      "agent_or_actor": "string",
      "section_affected": null,
      "status": "success | partial | failed | skipped | pending",
      "input_sections": [],
      "output_sections": [],
      "flags_raised": [],
      "flags_cleared": [],
      "duration_ms": null,
      "input_hash": null,
      "output_hash": null,
      "error_code": null,
      "error_message": null,
      "notes": null
    }
  ]
}

Although Pipeline Log is registered here as §4.18, the rendered file position is always last — see §2.1. The split between registry order and file position is intentional: the registry orders sections by topic; the file orders them so readers see the most recent run history at the bottom.


§ 4.19 — Custom Calculations ​

ID: custom_calculations
Header: ## Custom Calculations {#custom_calculations}
Purpose: User-defined financial calculations specific to this deal, investor, or analysis need. Any metric, ratio, or derived figure not covered by the standard sections lives here — supplemental return metrics, deal-specific ratios, portfolio comparisons, preferred equity waterfall components, or any calculation the user needs.
Written by: user, wizard, any agent
Required for pipeline stage: Never required; optional at any stage
Dependencies: Any section (via reference paths)

Design principle: A custom calculation is defined by its inputs (which may be literal values or live references to standard section fields using dot-path notation) and a plain-English formula description. AI tools can compute the result from the description; the engine can compute it if formula_expression is provided and parseable. The result is stored alongside the definition so the file is always self-contained. Recalculation is triggered whenever a referenced section is updated.

Live reference syntax: "source_ref": "noi_model.net_operating_income" — the engine substitutes the current value of that field into the calculation. If the field is null or the section doesn't exist yet, status is set to awaiting_inputs and result remains null.

json
{
  "_meta": { "...": "see §2.5" },
  "_notes": null,
  "calculations": [
    {
      "calc_id": "string",
      "label": "string",
      "description": "string",
      "category": "returns | debt_metrics | income | expense | ratio | sensitivity | portfolio | waterfall | tax | other",
      "inputs": [
        {
          "variable": "string",
          "label": "string",
          "value": null,
          "source_ref": "string | null",
          "is_live_ref": true
        }
      ],
      "formula_description": "string",
      "formula_expression": "string | null",
      "result": null,
      "result_unit": "pct | dollar | dollar_per_unit | dollar_per_sqft | ratio | years | months | bps | count | string | other",
      "result_label": "string",
      "result_interpretation": "string | null",
      "thresholds": {
        "target": null,
        "minimum": null,
        "maximum": null
      },
      "passes_threshold": null,
      "status": "computed | awaiting_inputs | error | pending",
      "error_message": null,
      "written_by": "user | agent | engine",
      "timestamp": "ISO8601",
      "notes": null
    }
  ]
}

Common examples:

Use CaseFormula DescriptionInputs
True all-in yieldNOI ÷ (purchase price + closing costs + reserves)valuation.purchase_price, sources_uses.closing_costs.total, sources_uses.uses.operating_reserves, noi_model.net_operating_income
Cash-on-cash in a specific yearYear N levered cash flow ÷ total equity investeddcf.annual_cash_flows[N].net_cash_flow_levered, sources_uses.equity_metrics.equity_total
NOI/door vs. portfolio averageSubject NOI/door ÷ portfolio average NOI/doornoi_model.noi_per_unit, user-supplied portfolio_avg_noi_per_door
Preferred equity coverageNOI − senior debt service − preferred return annualnoi_model.net_operating_income, debt_structure.annual_debt_service, user-supplied pref_return_annual
Loan constantAnnual debt service ÷ loan amountdebt_structure.annual_debt_service, debt_structure.loan_amount
Gross rent multiplierPurchase price ÷ gross annual rentsvaluation.purchase_price, rent_roll.gross_potential_rent_annual

§ 4.20 — Custom Scenarios ​

ID: custom_scenarios
Header: ## Custom Scenarios {#custom_scenarios}
Purpose: User-defined what-if analyses that go beyond the standard stress test matrix. These may model specific market events, construction delays, tenant losses, rate cap expiry, partnership changes, or any deal-specific situation the standard scenarios don't capture.
Written by: user, wizard, agents
Required for pipeline stage: Never required; optional at any stage
Dependencies: noi_model, debt_structure, dcf, assumptions (for baseline values to override)

Design principle: A custom scenario defines a named set of overrides against a baseline. Overrides reference keys from the assumptions registry or field paths in standard sections. The engine recomputes affected metrics with the overrides applied and stores results alongside the scenario definition. Every scenario is fully reproducible from baseline + overrides — no hidden state.

The sensitivity_sweeps array supports single-variable sweeps: the user specifies a range of values for one assumption and the engine computes the target metric at each point, producing a result series for charting or tabular display.

json
{
  "_meta": { "...": "see §2.5" },
  "_notes": null,
  "scenarios": [
    {
      "scenario_id": "string",
      "label": "string",
      "description": "string",
      "rationale": "string | null",
      "type": "stress | upside | sensitivity | regulatory | partnership | construction | market_event | tenant_event | rate | exit | tax | other",
      "based_on": "base_case | mild_stress | moderate_stress | string",
      "overrides": [
        {
          "assumption_key": "string",
          "label": "string",
          "section_ref": "string | null",
          "field_ref": "string | null",
          "original_value": null,
          "override_value": null,
          "override_type": "absolute | pct_change | bps_change",
          "rationale": "string | null"
        }
      ],
      "narrative_setup": "string | null",
      "results": {
        "noi": null,
        "egi": null,
        "dscr": null,
        "ltv": null,
        "debt_yield": null,
        "irr": null,
        "equity_multiple": null,
        "exit_value": null,
        "cash_on_cash_yr1": null,
        "passes_min_dscr": null,
        "min_dscr_threshold": null,
        "additional_metrics": {}
      },
      "narrative_result": "string | null",
      "status": "computed | awaiting_inputs | pending",
      "written_by": "user | agent | engine",
      "timestamp": "ISO8601"
    }
  ],
  "sensitivity_sweeps": [
    {
      "sweep_id": "string",
      "label": "string",
      "description": "string | null",
      "variable_key": "string",
      "variable_label": "string",
      "variable_values": [],
      "output_metric": "dscr | irr | equity_multiple | noi | ltv | debt_yield | cash_on_cash | custom",
      "output_label": "string",
      "results": [],
      "chart_type": "line | bar | table | null",
      "status": "computed | pending"
    }
  ]
}

Common examples:

ScenarioTypeKey Override
Renovation budget overrun +20%constructionsources_uses.renovation_budget +20%
Anchor tenant vacatestenant_eventOverride specific tenant status to vacant; recompute NOI
Rate cap expires at year 3ratedebt_structure.interest_rate from capped to index+spread at year 3
Lease-up 6 months longer than underwrittenstressDelay year-1 revenue growth by 2 quarters
Partner buyout at year 2partnershipModel equity buyout cost; recalculate year 2+ IRR
City rezones — cap rate compressionmarket_eventvaluation.income_approach.cap_rate_applied down 50bps
1031 replacement pressure (must close)regulatoryOverride close deadline; model higher purchase price

§ 4.21 — Extension Sections ​

ID: x_{user_defined_id}
Header: ## {User Label} {#x_{user_defined_id}}
Purpose: Fully user-defined sections for content that does not fit any standard section or the custom_calculations/custom_scenarios frameworks. Extension sections have no enforced schema — they are free-form containers that parsers collect without validating.
Written by: Any tool, user, or agent
Required for pipeline stage: Never required
Dependencies: None (extension sections are isolated by design)

When to use an extension section instead of custom_calculations/custom_scenarios:

  • The content is narrative-first, not calculation-first (e.g., a renovation scope document, a lender outreach tracker, a lease negotiation log)
  • The schema doesn't fit the override-a-baseline model
  • The user is prototyping a new section type before it standardizes

Rules:

  1. Section IDs MUST begin with x_ — this is the machine signal that the section is non-standard
  2. Parsers MUST collect extension sections under parsed.extensions[id] without error
  3. Validators MUST skip extension sections entirely — no schema checks
  4. Renders include extension sections in an "Additional Analysis" appendix when render_hint.include_in_output is true
  5. Cross-section consistency checks do not apply to extension sections
  6. The schema_version field signals a stable schema ready for standardization review — set it to a semver string when the schema has stabilized across deals
json
{
  "_meta": {
    "section": "x_your_id",
    "version": 1,
    "superseded": false,
    "source": "user",
    "agent_id": null,
    "agent_version": null,
    "actor": "string",
    "timestamp": "ISO8601",
    "confidence": "high | medium | low",
    "human_review_required": false,
    "flags": [],
    "input_hash": null,
    "notes": null
  },
  "_notes": null,
  "render_hint": {
    "include_in_output": false,
    "output_label": "string | null",
    "output_position": "appendix | after_section | null",
    "after_section_id": "string | null",
    "visible_tiers": ["screener", "analyst"]
  },
  "schema_version": null,
  "content": {}
}

The content object is entirely free-form. Any valid JSON is accepted.

Documented extension patterns (community-validated):

IDLabelUsed For
x_renovation_planRenovation PlanLine-item budget, contractor bids, scope notes, timeline
x_lender_matrixLender OutreachLenders contacted, terms quoted, status, next steps
x_partnership_structurePartnership / WaterfallGP/LP splits, promote tiers, preferred return details
x_1031_exchange1031 ExchangeID period deadlines, relinquished property, QI info
x_entitlement_logEntitlement LogZoning approvals, variance applications, hearing dates
x_lease_negotiationLease NegotiationTenant LOI terms, red lines, open issues, history
x_capex_scheduleCapEx ScheduleMulti-year capital plan by system, cost, year, priority
x_broker_analysisBroker UnderwritingBroker's own underwriting for comparison or reference
x_tax_analysisTax & DepreciationCost segregation, bonus depreciation, tax-adjusted returns

Graduating to standard: When an extension section appears frequently enough across deals that a consistent schema emerges, it is a candidate for a standard section in the next spec release. Submit via the spec changelog process with: the x_ schema you've been using, a description of the use case, and at least 3 example instances.


§ 4.22 — Gaps ​

ID: gaps
Header: ## Gaps {#gaps}
Purpose: A first-class inventory of the data the deal does not yet have. Blocks held back by missing inputs, fields filled from defaults, and items deferred for later rounds all surface here. Drives the refinement engine (Protocol §X) and is consulted by every validator that needs to know whether a missing value is intentional or accidental.
Written by: agent/L0a (initial scope), agent/L0b (refinement loop), manual (analyst notes), or system/gaps-maintainer (auto-maintenance hook in the editor)
Required for pipeline stage: Optional at every stage (a deal with no open gaps simply omits the section)
Dependencies: None — gaps reference other sections by ID/path but do not require them to exist
Schema: spec/schemas/section-gaps.schema.json

A gap entry names a missing or low-quality field by (section, field_path) and records why it is open (missing | illegible | out_of_scope | deferred | blocked_by_dependency | awaiting_external). Optional metadata declares the lowest stage the gap blocks, who owns resolving it, and how stale the entry is.

json
{
  "_meta": { "...": "see §2.5" },
  "_notes": null,
  "items": [
    {
      "section": "rent_roll",
      "reason": "missing",
      "blocks_stage": "screening",
      "first_seen": "2026-04-25T10:00:00Z",
      "last_checked": "2026-04-27T09:00:00Z",
      "owner": "agent/L0a",
      "note": "Awaiting unit-level rent roll from broker."
    },
    {
      "section": "noi_model",
      "field_path": "expense_ratio",
      "reason": "deferred",
      "blocks_stage": "full_underwrite",
      "owner": "manual",
      "note": "Using asset-class default until T-12 arrives."
    }
  ],
  "summary": {
    "total_open": 2,
    "blocking_current_stage": 1,
    "blocking_next_stage": 1
  }
}

Auto-maintenance. When a Tier-2 editor is invoked with the --maintain-gaps flag, the editor runs inferGaps() after every successful write and either updates the existing gaps section or creates one. The auto-write is stamped _meta.source: 'system/gaps-maintainer' and _meta.actor: 'system' so it is filterable from human-authored entries.

Validator codes. A provisional block with no matching gap entry triggers DQ-01; a partial block with no field_overrides triggers DQ-03; consuming a provisional block at a stage whose policy is halt triggers DQ-02; a stale entry (older than the institution's freshness threshold) triggers DQ-05. See Protocol §III.6a.


§ 4.23 — Components (Mixed-Use) ​

ID: components
Header: ## Components {#components}
Purpose: Decomposes a mixed-use property into its constituent uses, each keyed by its own asset class. A mixed-use property is two or more uses under one purchase price and one loan — apartments over ground-floor retail, an office podium under a hotel — and no single income model or denominator describes it. This section states each use's own income so that property-level figures foot as the sum of the components while use-level metrics stay honest. It is the one section whose presence is gated to a single asset_class.
Written by: wizard, manual, agent/L4-* (component subtotals only — an agent MUST NOT populate allocation_pct; see Allocation below)
Required for pipeline stage: Full underwriting, and only when asset_class is mixed_use
Dependencies: valuation, noi_model (the property-level figures the components foot into)
Schema: spec/schemas/section-components.schema.json
Introduced by: RFC 0019 (mixed-use composition).

The section is a bounded map keyed by the component's own asset class, not a variable-length array, so that every field path a calc pack reads stays static (e.g. components.retail.net_operating_income). Each entry is one use type, not one tenant: a property with two retail suites has a single retail component.

Normative rules (RFC 2119):

  • The components section MAY appear only when frontmatter.asset_class is mixed_use. A document with any other asset class carrying a components section MUST be rejected (CC-11). This keeps the section from becoming a general-purpose escape hatch.
  • Each key MUST be one of multifamily, retail, office, industrial, self_storage, hospitality, senior_housing, or student_housing, and each entry's component_class MUST equal its key. These are exactly the asset classes whose net_operating_income is computed on the standard effective_gross_income − total_operating_expenses basis. land MUST NOT be a component — its NOI model nets negative by design, and admitting it would let a carry burden silently reduce property income.
  • At least two components MUST be present. A single-component document is not mixed use and MUST declare that component's own asset class instead, where a real calc pack and defaults table already apply.
  • Each present component MUST state net_operating_income. A present component that omits a figure a property rollup consumes is an incomplete document, not a use with no income, and MUST raise a typed validation error rather than resolve to zero. An absent component — a class simply not listed — contributes nothing to any rollup.
  • A component MUST NOT carry its own debt_structure. This section models one property-level loan. Component-level financing (for example a separately-financed commercial condo) is expressed as a component-level capital_stack (§ 4.24, RFC 0026): a component MAY carry a capital_stack object of the same shape as the top-level section, validated by the same CS-* rules. The senior-reconciliation rule (generalized CC-03) applies only to the top-level stack — a component stack is that component's own financing and has no property-level debt_structure counterpart to reconcile with.

Footing. Property noi_model.net_operating_income MUST equal the sum of the component net_operating_income figures (CC-12). Because every admitted class computes NOI on the same basis, this sum is well-defined with no special case.

Operating-business components. hospitality, senior_housing, and student_housing are operating businesses. Where a component's own calc pack strikes a model-level intermediate subtotal above NOI — gross_operating_profit for hospitality, total_labor_expense for senior_housing — that subtotal MAY be carried in the component and is surfaced per component. These subtotals sit at different points in their respective waterfalls and MUST NOT be blended into a single synthesized property-level intermediate. A property-level gross_operating_profit MAY be reported only as a memo summing the components that declare one; a component that has none contributes N/A, never 0.

Allocation. One purchase price and one loan cover the whole property, so use-level intensive metrics (price per apartment, loan per retail square foot) require splitting those single figures across components. There is no deterministic way to derive that split — income share, area share, and appraised-value share all disagree, and the choice is an underwriter's judgment. Therefore allocation_pct is an input, never a derivation: it is user-supplied, MUST sum to 1.0 across present components within 0.0001, and an agent MUST NOT populate it. When allocation_pct is absent, every use-level intensive metric MUST evaluate to null rather than fall back to an area- or income-share guess.

json
{
  "_meta": { "...": "see §2.5" },
  "_notes": null,
  "multifamily": {
    "component_class": "multifamily",
    "effective_gross_income": 2180000,
    "operating_expenses": 880000,
    "net_operating_income": 1300000,
    "total_units": 120,
    "nra_sqft": 96000,
    "allocation_pct": 0.78
  },
  "retail": {
    "component_class": "retail",
    "effective_gross_income": 520000,
    "operating_expenses": 148000,
    "net_operating_income": 372000,
    "nra_sqft": 18000,
    "allocation_pct": 0.22
  }
}

In this example property NOI is 1300000 + 372000 = 1672000, which noi_model.net_operating_income MUST equal, and the allocations sum to 1.0.


§ 4.24 — Capital Stack ​

ID: capital_stack
Header: ## Capital Stack {#capital_stack}
Purpose: States a deal's full capital structure as an ordered set of typed tranches — senior debt under mezzanine under preferred equity under common equity, with bridge or seller financing where present — and the stack-aware sizing figures a lender underwrites to. debt_structure (§ 4.7) models exactly one loan; this section is where a multi-tranche stack lives, so that per-layer coverage and attachment-point risk are visible rather than collapsed into a single mislabeled DSCR. The stack is asset-class independent.
Written by: wizard, manual, agent/L4-* (tranche amounts and terms are user-supplied capital terms — an agent MUST NOT invent a tranche rate, amount, or a stated sizing value)
Required for pipeline stage: Optional; used whenever a deal carries more than one capital tranche.
Dependencies: debt_structure, sources_uses (the senior tranche reconciles with both — see CC-03)
Schema: spec/schemas/section-capital-stack.schema.json
Introduced by: RFC 0026 (capital stack).

The section has two parts: an ordered tranches array and a sizing array of stated figures a verifier recomputes. Unlike components (§ 4.23), which must be a bounded map because a calc pack reads its fields by static path, capital_stack is a state-and-verify structure (RFC 0021 § 6): the Tier‑3 calc engine never reads it, so a variable-length array is safe. Stated sizing figures are recomputed by a deterministic verifier (verifyCapitalStack, a sibling of verifyRollup) over a fixed, closed function vocabulary — not by a pack formula and not by any calc-engine primitive. This is what lets the stack carry an arbitrary number of tranches (including two mezzanine notes) without the sandbox iterating.

Tranches. Each entry is a typed object:

  • id — document-local, unique within the section, free-form (senior, mezz_a, mezz_b). Two mezzanine notes are simply two tranches.
  • class — one of senior_debt, mezzanine_debt, preferred_equity, common_equity, bridge, seller_financing, other_debt. Non-extensible.
  • position — integer, 1 = most senior. Positions MUST be unique within the stack.
  • amount — the committed dollar amount.
  • rate — the coupon (debt) or preferred return (pref), as a fraction. REQUIRED for every debt tranche and for preferred_equity; MUST NOT appear on common_equity.
  • accrual — cash (current‑pay; enters cash coverage), accrued (PIK; compounds on balance and does not enter cash coverage), or split (RFC 0050; preferred-equity current-pay plus accrued components). split is permitted only on preferred_equity in Protocol 2.x.
  • cash_rate — fraction required only when accrual is split; the current-pay component that enters cash coverage. MUST NOT appear for another accrual mode.
  • accrued_rate — fraction required only when accrual is split; the accrued/PIK component that does not enter cash coverage. MUST NOT appear for another accrual mode.
  • For accrual: "split", rate is required and MUST equal cash_rate + accrued_rate; disagreement is CS-02b.
  • amortization_months, io_months, term_months — debt terms, from which the tranche's own annual debt service is derived deterministically.

Normative rules (RFC 2119):

  • The capital_stack section is OPTIONAL and asset-class independent. A document with no capital_stack behaves exactly as before this RFC; every single-loan metric is unchanged.
  • The stack is one point in time (stated by RFC 0033). The section states the capitalization contemporaneous with the operating year the sizing verbs read (noi_model.net_operating_income) — for a deal underwritten to stabilization, the stabilized (permanent) stack. Facilities that do not coexist at that point MUST NOT be stated as concurrent tranches: a construction loan and the permanent takeout that retires it are one senior position in time sequence, and stating both double-counts the senior capital and misstates every attachment point above it. A producer underwriting a ground-up development SHOULD emit the stabilized stack — the takeout senior plus the layers that survive into stabilization — and MUST NOT add the retired construction facility to it. A bridge tranche is unaffected: a deal underwritten during its bridge period states the bridge-period capitalization, which is the point in time its NOI describes. Phased stacks (construction → takeout, draw schedules, as-is/as-stabilized dual sizing) are deferred with the multi-period spine (RFC 0026 § D; RFC 0033).
  • Each tranche MUST state id, class, position, and amount; id and position MUST be unique within the section. A malformed or duplicate-keyed tranche MUST be rejected (CS-01).
  • Every debt tranche and every preferred_equity tranche MUST state a rate; a common_equity tranche MUST NOT (CS-02).
  • When a capital_stack is present, its senior_debt tranche MUST reconcile with debt_structure and the sources_uses senior bucket — same amount, same rate. This is the generalized CC-03: the senior view is stated once and agrees everywhere.
  • Sizing is stated and verified, never trusted. Each sizing entry names a closed fn, a selector, and a stated value; a verifier recomputes fn and compares. Disagreement is CS-SIZING-DISAGREES and the result is failed, never a silent pass. A figure that reads a tranche field the document does not supply is unverifiable, never 0.
  • The distribution waterfall is out of scope for this version. A capital_stack that attempts to encode distribution tiers, promote, IRR hurdles, or catch-up MUST be refused (CS-WATERFALL-UNSUPPORTED) with guidance to x_partnership_structure; the multi-period distribution waterfall is deferred (RFC 0026 § E) pending a hold-period cash-flow primitive.

Sizing vocabulary (closed, non-extensible). Each figure selects tranches with over (a single tranche id), through (a position, cumulative through that layer), or * (the whole stack):

fnselectorrecomputation
coverageovernoi_model.net_operating_income ÷ that tranche's annual debt service
blended_coveragethroughNOI ÷ Σ cash-pay debt service at or above the position (accrued/PIK excluded; a split preferred tranche contributes amount × cash_rate)
debt_yield_throughthroughNOI ÷ cumulative debt balance through the position — the attachment-point yield
ltc_through / ltv_throughthroughcumulative balance through the layer ÷ total cost / value
weighted_cost*amount-weighted average rate across the stack; a split preferred tranche uses its full rate, not only cash_rate

An accrued/PIK tranche contributes zero to blended_coverage but its balance still counts in debt_yield_through — accrual changes cash coverage, not the capital ahead of you. A split preferred tranche contributes only its cash_rate service to coverage and its full stated rate to weighted_cost.

json
{
  "_meta": { "...": "see §2.5" },
  "tranches": [
    { "id": "senior", "class": "senior_debt",      "position": 1, "amount": 26000000, "rate": 0.0625, "amortization_months": 360, "io_months": 24, "term_months": 120, "accrual": "cash" },
    { "id": "mezz_a", "class": "mezzanine_debt",    "position": 2, "amount": 6000000,  "rate": 0.11,   "amortization_months": 0,   "io_months": 120, "term_months": 120, "accrual": "cash" },
    { "id": "pref",   "class": "preferred_equity",  "position": 3, "amount": 4000000,  "rate": 0.09,   "accrual": "accrued" },
    { "id": "common", "class": "common_equity",     "position": 4, "amount": 4000000 }
  ],
  "sizing": [
    { "id": "senior_dscr",     "fn": "coverage",           "over": "senior",  "value": 1.85 },
    { "id": "combined_dscr",   "fn": "blended_coverage",   "through": 2,       "value": 1.14 },
    { "id": "mezz_debt_yield", "fn": "debt_yield_through", "through": 2,       "value": 0.080 },
    { "id": "wacc",            "fn": "weighted_cost",      "over": "*",        "value": 0.0731 }
  ]
}

Here the pref tranche is accrued, so it is excluded from combined_dscr but its balance is not part of mezz_debt_yield (which is a debt-only attachment metric through position 2). The senior tranche's amount and rate reconcile with debt_structure under CC-03.

§ 4.25 — Lease-Up Schedule ​

ID: lease_up_schedule
Header: ## Lease-Up Schedule {#lease_up_schedule}
Purpose: Models the trajectory from current to stabilized rents over a defined window. rent_roll (§ 4.2) is a snapshot in time and noi_model (§ 4.4) a stabilized projection; the path between them — turnover pace, market-rent assumption, vacancy during lease-up, TI/LC capital — is the entire thesis of a value-add or ground-up deal, and this section is where that path lives as structure rather than _notes. The section is asset-class independent and multi-variant: base / upside / downside scenarios coexist under variant= exactly as stress_tests variants do.
Written by: wizard, manual, agent/L4-* (the assumption set and stated figures are the deal thesis — an agent MAY draft a schedule but the host stamps agent/<id> and the figures remain subject to verification)
Required for pipeline stage: Optional; used whenever the underwriting assumes a lease-up path. Never required — a stabilized acquisition legitimately has no lease-up story.
Dependencies: rent_roll (soft — see LU-04), noi_model (CC-15), property (the § XIII size denominator for occupancy_rate verification)
Schema: spec/schemas/section-lease-up-schedule.schema.json
Introduced by: RFC 0008 (lease-up modeling).

Like capital_stack (§ 4.24), this is a state-and-verify structure (RFC 0021 § 6): the Tier‑3 calc engine never reads the schedule by pack formula, so the variable-length period array is safe, and every stated aggregate is recomputed by a deterministic verifier (verifyLeaseUpSchedule, a sibling of verifyCapitalStack) over a fixed, closed recompute vocabulary. The schedule is data, not formulas — no calc-engine iteration, time axis, or new builtin is involved. Cells are read by the verifier and by host code, not by pack formulas: calc paths are flat identifiers, so lease_up_schedule.schedule[5].rent_revenue does not parse and raises CALC-PARSE-001 (corrected by RFC 0055; the claim that it was addressable was never true).

Fields.

  • model_type — natural_turnover (an occupied asset re-leasing at market through turnover; value-add) or absorption_curve (filling vacant or newly-built space; ground-up). Closed. The type gates nothing structurally.
  • period_granularity — monthly or quarterly, uniform per schedule.
  • stabilization_target — the period by which the deal is underwritten to stabilize, in the declared grammar.
  • assumptions — the driving assumption set (monthly_turnover_rate, market_rent_psf_at_stabilization, vacancy_during_lease_up, concession_months_per_lease, tenant_improvement_psf, leasing_commission_rate). All rates are fractions (0.04, never 4). These are the deal thesis, not defaultable background: nothing here enters the cascade tables.
  • schedule — one entry per period: period, occupied_sf, leased_sf, in_place_rent_psf, market_rent_psf, vacancy_rate, rent_revenue, concessions (negative), ti_lc_capex (negative), net_cash_flow.
  • stabilized_summary — the stated endpoint: occupied_sf, occupancy_rate, annualized_egi, annualized_noi.

Normative rules (RFC 2119):

  • The section is OPTIONAL. A document with no lease_up_schedule behaves exactly as before this RFC; no pipeline stage requires it (STAGE_REQUIREMENTS untouched).
  • Period grammar (LU-01, error). A period is YYYY-Qn (quarterly) or YYYY-MM (monthly), matching the declared period_granularity. One schedule uses one cadence; mixed granularity within a schedule MUST be rejected.
  • Contiguity (LU-02, error). Periods MUST be strictly increasing and gap-free. A schedule that skips a quarter is asserting something it does not say; a period with no activity states its figures, even when they are zero.
  • Non-empty, non-inverted (LU-03, error). schedule MUST be non-empty, and stabilization_target MUST NOT be earlier than the first period.
  • Turnover needs a starting point (LU-04, warning). model_type: natural_turnover with no rent_roll present in the document is a warning, not a refusal — a compose-time fragment may carry the schedule without the roll.
  • Stated figures are verified, never trusted. A verifier MUST recompute, three-state (verified / failed / unverifiable), quantizing both sides at the same quantum (Protocol § VIII.5 posture): each period's net_cash_flow = rent_revenue + concessions + ti_lc_capex (a period omitting a stated component is unverifiable for that row, never failed); stabilized_summary.occupied_sf against the final period; and stabilized_summary.occupancy_rate = final occupied SF ÷ the § XIII size denominator (no resolvable square-foot denominator → unverifiable, never a guess).
  • Endpoint agreement (CC-15, warning, § 5.3). The base variant's stabilized_summary.annualized_noi MUST agree with noi_model.net_operating_income within LEASE_UP_STABILIZED_TOLERANCE (2%, a named exported constant). The tolerance is deliberate, not softness: unlike CC-01 (one number restated in two places, exact), the trajectory endpoint and the stabilized-year projection are two different models of stabilization, and demanding exact agreement would force producers to hand-tune one to echo the other — destroying the independent-model signal the check exists to read. Non-base variants are exempt: a downside scenario is supposed to disagree with stabilized NOI.
json
{
  "_meta": { "...": "see §2.5" },
  "model_type": "natural_turnover",
  "period_granularity": "quarterly",
  "stabilization_target": "2027-Q4",
  "assumptions": {
    "monthly_turnover_rate": 0.04,
    "market_rent_psf_at_stabilization": 22.00,
    "vacancy_during_lease_up": 0.18,
    "concession_months_per_lease": 1,
    "tenant_improvement_psf": 35.00,
    "leasing_commission_rate": 0.06
  },
  "schedule": [
    { "period": "2026-Q3", "occupied_sf": 31000, "leased_sf": 31000, "in_place_rent_psf": 21.50, "market_rent_psf": 22.00, "vacancy_rate": 0.27, "rent_revenue": 166375, "concessions": -5400, "ti_lc_capex": -42500, "net_cash_flow": 118475 },
    { "period": "2026-Q4", "occupied_sf": 34000, "leased_sf": 35500, "in_place_rent_psf": 21.65, "market_rent_psf": 22.00, "vacancy_rate": 0.20, "rent_revenue": 184025, "concessions": -8100, "ti_lc_capex": -52500, "net_cash_flow": 123425 }
  ],
  "stabilized_summary": {
    "occupied_sf": 40500,
    "occupancy_rate": 0.953,
    "annualized_egi": 858000,
    "annualized_noi": 478000
  }
}

Deliberately deferred (RFC 0008). Excel emit of a Lease-Up sheet; a dcf coupling (derive_dcf_from_lease_up); defaults-table entries; and a shared period-schedule primitive. If a later module (e.g. a hospitality RevPAR ramp) wants a period schedule, it SHOULD reuse this section's period grammar rather than invent a second one — noted so the two cannot drift, but no abstraction is built ahead of the second consumer.


§ 4.26 — Cash Flow Series ​

ID: cash_flow_series
Header: ## Cash Flow Series {#cash_flow_series}
Purpose: A dated, irregular cash-flow series — ISO-8601 calendar dates and signed amounts — for hold-period modeling on real calendars: mid-month closings, irregular draws, anniversary exits. lease_up_schedule (§ 4.25) carries period-cadence rows on a uniform monthly/quarterly axis; this section carries flows anchored to calendar dates, which is a different contract (cadence vs. calendar) and deliberately a different section. The section is asset-class independent and multi-variant: base / upside / downside scenarios coexist under variant= exactly as stress_tests variants do.
Written by: wizard, manual, agent/L4-* (an agent MAY draft a series — e.g. from a vendor projection engine — but the host stamps agent/<id> and the stated figures remain subject to verification)
Required for pipeline stage: Optional; never required. A deal underwritten on stabilized annual figures alone legitimately has no dated series.
Dependencies: none required; dcf (§ 4.9) is a natural sibling but no cross-check is defined by RFC 0034 (deferred — see the RFC's unresolved questions).
Schema: spec/schemas/section-cash-flow-series.schema.json
Introduced by: RFC 0034 (calendar-anchored cash flows).

Like capital_stack (§ 4.24) and lease_up_schedule (§ 4.25), this is a state-and-verify structure (RFC 0021 § 6): the Tier-3 calc engine never reads the series by pack formula, so the variable-length array is safe, and every stated aggregate is recomputed by a deterministic verifier (verifyCashFlowSeries, a sibling of verifyCapitalStack and verifyLeaseUpSchedule) over a fixed, closed recompute vocabulary (Protocol § VIII.9). The series is data, not formulas. Rows are read by the verifier and by host code, not by pack formulas: calc paths are flat identifiers, so cash_flow_series.series[3].amount does not parse and raises CALC-PARSE-001 (corrected by RFC 0055).

Fields.

  • label — free text ("Levered hold-period cash flow"). Optional.
  • day_count — one of the closed Protocol § VIII.9.1 registry: actual/365f (the default when absent), actual/360, or 30/360us. Governs every year-fraction this section's metrics use.
  • series — one entry per flow: date (ISO-8601 YYYY-MM-DD), amount (signed; outflows negative), optional kind (closed: acquisition | operating | capex | debt_service | refinance | disposition | other), optional label. kind is advisory taxonomy for renderers and rollups; it gates no rule.
  • stated_metrics — the stated aggregates, all optional, each verified when present:
    • total_net — Σ amount (currency).
    • moic — Σ inflows ÷ |Σ outflows| (multiple).
    • xnpv — { "rate": <fraction>, "value": <number> } (currency; the rate is a fraction, 0.08 never 8).
    • xirr — the annualized rate (fraction).

Normative rules (RFC 2119):

  • The section is OPTIONAL. A document with no cash_flow_series behaves exactly as before RFC 0034; no pipeline stage requires it (STAGE_CONTRACT untouched).
  • Row grammar (CF-01, error). Every date MUST be a valid ISO-8601 calendar date (YYYY-MM-DD naming a real day — 2026-02-30 refuses); every amount MUST be a finite number. An unknown day_count or kind MUST be refused (closed enums).
  • Ordering (CF-02, error). series MUST be non-empty and dates MUST be non-decreasing. Same-day flows are legal (a closing and its first draw share a date). They MUST NOT be merged, netted, reordered or collapsed merely because their dates are equal — each row keeps its identity through existing row-binding surfaces. Duplicate dates alone MUST NOT produce PS-02. Ordinary date selectors require a unique matching row and otherwise refuse CALC-PERIOD-002 (Protocol §VIII.2a, RFC 0062); this adds no numeric-index calc grammar. The first row's date is the series anchor.
  • A stated xirr needs a sign change (CF-03, error). A stated xirr on a series whose amounts do not include at least one negative and at least one positive value MUST be refused: the stated number cannot be the root of anything.
  • Stated figures are verified, never trusted. A verifier MUST recompute, three-state (verified / failed / unverifiable), quantizing both sides at the same quantum (Protocol § VIII.5 posture; § VIII.9.4): total_net and moic directly; xnpv closed-form at the stated rate (§ VIII.9.2); xirr by the § VIII.9.3 procedure. A stated metric whose recomputation raises (an xirr that fails to bracket) is failed — the document asserts a number the procedure cannot produce. A moic stated on a series with no outflows is unverifiable (an undefined ratio is not evidence of tampering). A metric absent from stated_metrics is simply not checked.
json
{
  "_meta": { "...": "see §2.5" },
  "label": "Levered hold-period cash flow",
  "day_count": "actual/365f",
  "series": [
    { "date": "2026-03-17", "amount": -14250000, "kind": "acquisition", "label": "Close" },
    { "date": "2026-09-30", "amount": 412000, "kind": "operating" },
    { "date": "2027-03-31", "amount": 431000, "kind": "operating" },
    { "date": "2027-06-15", "amount": -350000, "kind": "capex", "label": "Roof + amenity" },
    { "date": "2031-03-17", "amount": 19800000, "kind": "disposition", "label": "Exit" }
  ],
  "stated_metrics": {
    "total_net": 6043000,
    "moic": 1.4139,
    "xnpv": { "rate": 0.06, "value": 1022812.04 },
    "xirr": 0.075239
  }
}

(The stated metrics above were computed by the reference verifier, not asserted — moic at the 4-decimal ratio quantum, xnpv at the currency quantum, xirr at the 6-decimal rate quantum.)

Deliberately deferred (RFC 0034). Excel emit — Excel's XIRR is Newton-seeded, so a live =XIRR(...) formula cannot hold the bit-exact parity boundary the ROUND-wrapped closed-form formulas do; if a Cash Flow sheet is ever emitted, stated/computed metrics land as literals, by an RFC that takes that exception knowingly. Also deferred: a dcf cross-check and defaults-table entries. The RFC 0026 Phase 2 distribution waterfall — whose stated precondition this section removed — was taken up as RFC 0035 (§ 4.27).


§ 4.27 — Distribution Waterfall ​

ID: distribution_waterfall
Header: ## Distribution Waterfall {#distribution_waterfall}
Purpose: The tiered split of a deal's equity cash flows between the aggregate LP and the GP — return of capital, a preferred return accrued under a day-count convention, a GP catch-up, and residual promote splits. This is the partnership half of the underwriting: capital_stack (§ 4.24) is the liability side at one point in time (RFC 0033), and this section is the equity side over the hold — which is why the § 4.24 CS-WATERFALL-UNSUPPORTED boundary stays: partnership tiers never live inside the debt stack. The section is asset-class independent and multi-variant (variant= per the stress_tests rules).
Written by: wizard, manual, agent/L4-* — with one prohibition mirroring § 4.24's: an agent MUST NOT invent tier terms, splits, or hurdle levels; they are partnership terms from the operating agreement.
Required for pipeline stage: Optional; never required. A single-party deal legitimately has no waterfall.
Dependencies: cash_flow_series (§ 4.26) — the referenced variant is the cash vector (WF-02).
Schema: spec/schemas/section-distribution-waterfall.schema.json
Introduced by: RFC 0035 (distribution waterfall); the shape was documented and deferred by RFC 0026 §E. until_lp_irr by RFC 0036. clawback by RFC 0059.

Like § 4.24 – § 4.26 this is a state-and-verify structure (RFC 0021 § 6) — the fourth: the document states the ladder and the outcomes, and a deterministic verifier (verifyWaterfall) recomputes the entire allocation — period by period, tier by tier — by the normative Protocol § VIII.10 procedure, so two conforming engines agree on the promote. The Tier-3 calc engine is untouched.

Fields.

  • cash_flow_ref — { "variant": "<name>" }: the § 4.26 variant holding the equity cash vector. Negative amounts are contributions (capital calls); positive amounts are distributable cash. The series' own day_count governs every year fraction here.
  • equity_split — { "lp": 0.90, "gp": 0.10 }: each party's share of every contribution, summing to 1.0 (ratio quantum). The GP share is co-invest; the promote comes from the tiers.
  • tiers — the ordered ladder, closed type vocabulary, in this order (any of the first three MAY be absent; each of the first three appears at most once; at least one split):
    1. return_of_capital — pro-rata by unreturned contributed capital.
    2. preferred_return — { "rate": 0.09, "accrual": "simple" | "compound_annual" }: accrues on each party's unreturned capital pari passu; paid pro-rata by accrued balance. rate is a fraction in (0, 1).
    3. catch_up — { "gp_share": 1.0, "target_promote": 0.20 }: gp_share of this tier's cash goes to the GP until GP cumulative profit reaches target_promote of total profit distributed. Profit means distributions above returned capital; pref receipts count as profit (Protocol § VIII.10 — excluding them would give a catch-up that follows the pref tier capacity zero forever). gp_share MUST exceed target_promote (otherwise the tier can never fill — a grammar refusal, not a runtime case).
    4. split — { "lp_share": 0.80, "gp_share": 0.20, "until_lp_em": 1.5, "until_lp_irr": 0.12 }: residual split; lp_share + gp_share MUST sum to 1.0 (ratio quantum). The optional until_lp_em (> 0) caps the tier where cumulative LP distributions reach that multiple of LP contributions to date. The optional until_lp_irr (a fraction in (0, 1)) caps the tier where the LP's dated flows — every contribution and distribution to date, including this tier's payment at this row's date — reach that internal rate of return under the series' day count (Protocol § VIII.10, the hurdle balance: closed-form, never a nested solve). until_lp_em and until_lp_irr MAY both be stated on one tier; the tier then ends only when both hurdles are met (the larger capacity governs) unless hurdle_mode: "any" is specified (the smaller capacity governs; RFC 0051). Stating hurdle_mode on a tier that does not state both hurdles is an error (WF-01). A capped split MUST have lp_share > 0 whichever hurdle caps it (a cap on a tier that pays the LP nothing can never bind). The final tier MUST be a split uncapped by either field. Across the ladder, successive stated until_lp_irr values MUST strictly increase, as MUST successive stated until_lp_em values (a later tier hurdled at or below an earlier one has capacity zero by construction and would pay nothing, silently).
  • clawback — optional (RFC 0059): the terminal true-up provision. { "basis": "lp_preferred_shortfall | lp_irr_floor | lp_em_floor", "floor_rate": null, "floor_multiple": null, "net_of_tax_rate": null, "cap": "promote_received" }. basis is a closed vocabulary; lp_irr_floor requires floor_rate and lp_em_floor requires floor_multiple (WF-10), lp_preferred_shortfall requires a preferred_return tier to fall short of (WF-11). cap MUST be "promote_received" (WF-12) — a GP cannot owe back more promote than it received, and any other cap is a different instrument. net_of_tax_rate is a fraction in [0,1) (WF-13), stated and applied, never derived: this section does not model the GP's tax position. A provision stated over a series with no distribution is WF-15, a warning, since the provision may legitimately precede the exit.
  • stated_outcomes — the headline claims, all optional, each verified when present: lp / gp objects with contributions, distributions, moic, xirr; plus promote_total, profit_total and clawback_amount (RFC 0059). A stated clawback_amount on a waterfall declaring no clawback provision is unverifiable, not a disagreement — there is no recomputed figure to compare it to.
  • stated_schedule — optional: one entry per distribution date — { "date": "YYYY-MM-DD", "by_tier": [{ "tier": 0, "lp": 118475.0, "gp": 13163.9 }] }, tier indexes into tiers. When present it MUST match the recomputed allocation cell-for-cell (an absent cell reads as 0); when absent only stated_outcomes are checked. Either way the verifier recomputes the full allocation — the schedule is display detail, not the source of truth.

Normative rules (RFC 2119):

  • The section is OPTIONAL; no pipeline stage requires it (STAGE_CONTRACT untouched).
  • Ladder grammar (WF-01, error). As above: closed types, singleton and ordering rules, a terminal split uncapped by either hurdle, shares in [0, 1] with the stated sums, rate in (0, 1), until_lp_em > 0, until_lp_irr in (0, 1), capped splits paying the LP, hurdle_mode in ["both", "any"] only when both hurdles are stated, successive hurdles of one kind strictly increasing (compared at the ratio / rate quantum respectively), gp_share > target_promote, and equity_split summing to 1.0.
  • The reference must resolve (WF-02, error). cash_flow_ref.variant MUST name a variant present in the document's cash_flow_series. A waterfall without its cash vector is not reviewable.
  • A waterfall needs capital (WF-03, error). The referenced series MUST contain at least one negative amount; over pure inflows every tier is vacuous.
  • Stated figures are verified, never trusted. verifyWaterfall recomputes by Protocol § VIII.10, three-state, both sides quantized at the § VIII.9.4 quanta ($ → 2, x → 4, % → 6): outcomes and schedule cells against the recomputed allocation; a stated xirr whose recomputation raises (§ VIII.9.3 refusal — e.g. a zero-contribution party) is failed; a moic over zero contributions is unverifiable; an unresolvable or structurally invalid referenced series makes every stated figure unverifiable, never a guess.

Deliberately deferred (RFC 0035, RFC 0036, RFC 0051, RFC 0059). N-party splits, Excel emit (the § 4.26 literals posture), a capital_stack cross-check (waterfall contributions vs. the common-equity tranche), and GP-side hurdles (RFC 0051 lifted hurdle_mode: "any" into the normative contract; RFC 0059 took clawback as a terminal true-up, leaving per-period crystallization and interim promote escrows deferred).


Part V — Validation Rules ​

5.1 Pipeline Stage Completeness Requirements ​

The following sections must be present (non-null) before advancing to each stage:

StageRequired Sections
Scopeproperty (see also the field-level scope requirements, Protocol §III.6a / DQ-04)
Screening+ debt_structure (amounts only), validation
Term Sheet+ rent_roll, borrower_sponsor (principals only), preliminary_sizing
Full Underwrite+ operating_statement (T-12), noi_model, valuation, sources_uses, market_analysis
Credit Approval+ dcf, stress_tests, risk_assessment, compliance, assumptions
Closing+ due_diligence (all items cleared), debt_structure (final terms)
Portfolio MonitoringSame section list as Closing

This table is machine-checked (RFC 0028): a validator MUST compute stage_readiness from it, and MUST report each section the declareddeal_stage requires but the file lacks as a DQ-06 info issue — a declared stage is where the deal is going, so the gap is reportable but not refusable. A missing property section is CC-14 (§5.3) instead, at every stage. Freshness obligations that earlier revisions listed under Monitoring ("updated operating_statement annually", "re-run validation") are not expressible as section presence; staleness belongs to the DQ-05 machinery (§4.22) and institution policy.

Class overlays (RFC 0029). The base lists assume an income property with direct tenancy. Two asset classes diverge structurally, and for them a validator MUST adjust the requirements as follows — exhaustively; an asset class not listed here (including an unrecognized one) takes the base lists verbatim:

asset_classAdjustment
landrent_roll and operating_statement are exempt at every stage: raw land has no tenancy and no operating statement — its carry lives in noi_model and dcf, and nothing stands in for the exempted sections.
mixed_usecomponents substitutes for rent_roll and operating_statement: wherever a stage's base list requires either, the requirement is satisfied by (and only by) a components section, which §4.23 already makes the home of per-component income and operating detail.

A substitution is checkable — the substitute section is required at the stage where the replaced section would have been (a substitute replacing two sections is required once) — where an exemption simply removes the requirement. stage_readiness and DQ-06 MUST resolve through the same overlay, and CC-14 is unaffected: property is never exempt for any class. Section ids remain class-neutral containers otherwise: hospitality's rent_roll carries keys/ADR/segmentation, senior housing's carries unit/acuity mix, and no overlay entry exists for them because reuse is the design, not a gap.

5.2 Financial Validity Thresholds ​

These are the default thresholds. Institution configs may override.

MetricError BelowWarning BelowWarning AboveError Above
DSCR1.00x1.20x——
LTV——75%85%
Debt Yield—7%——
Going-in Cap Rate—3%15%—
Vacancy Rate—2%40%—
OpEx Ratio—20%70%—
Levered IRR—5%40%—
Equity Multiple—1.0x5.0x—
Annual Rent Growth——8%—
LTC (construction)——80%90%

5.3 Cross-Section Consistency Checks ​

Tools MUST run these after each section write:

Check IDDescriptionSections
CC-01Rent roll GPR must match OS GPR within 3%rent_roll, operating_statement
CC-02UW value in valuation must match value used for LTV in debt_structurevaluation, debt_structure
CC-03Senior loan reconciles: sources_uses senior loan, debt_structure.loan_amount, and (when present) the capital_stack senior_debt tranche must all matchsources_uses, debt_structure, capital_stack
CC-04Sources must equal uses in sources_usessources_uses
CC-05NOI used for DSCR must match noi_model.net_operating_income within 1%noi_model, debt_structure
CC-06DCF Year 1 NOI must be consistent with noi_model projectionsnoi_model, dcf
CC-07Exit cap rate in dcf must match stress test cap rate expansion scenariosdcf, stress_tests
CC-08Appraised value in due_diligence.appraisal must match valuation.appraised_valuedue_diligence, valuation
CC-09Annual debt service in stress_tests base case must match debt_structure.annual_debt_servicestress_tests, debt_structure
CC-10Purchase price in sources_uses.uses must match valuation.purchase_pricesources_uses, valuation
CC-11A components section may appear only when asset_class is mixed_use (RFC 0019)components, frontmatter
CC-12Property noi_model.net_operating_income must equal the sum of component net_operating_income (mixed-use)components, noi_model
CC-13The property section must state the primary size field for frontmatter.asset_class (Protocol §XIII.1) (RFC 0027)property, frontmatter
CC-14A deal-record document must have a property section (§4.1) (RFC 0028)property
CC-15The lease-up base variant's stabilized_summary.annualized_noi must agree with noi_model.net_operating_income within LEASE_UP_STABILIZED_TOLERANCE (2%) (RFC 0008)lease_up_schedule, noi_model
CC-16A section a cross-check reads is present as multiple variants and none resolves under the §5.3 preference order; the checks reading it were skipped (info) (RFC 0037)any

Resolution over variant maps (RFC 0037 / RFC 0040). A cross-check reads one block per section. Exclude component-role blocks and invalid roles before every selection step, including standalone/default/base/sole paths. For eligible variants, try (1) explicit check preference (t12 on CC-01's operating statement, appraisal on CC-08's due diligence); (2) unique check role; (3) unique primary; (4) default; (5) base; (6) sole eligible variant. CC-01 prefers detail on rent_roll; CC-02/03/05/09 prefer senior on debt_structure (CROSS_CHECK_ROLE_PREFERENCE). A consulted role collision MUST refuse immediately, without fallback. An earlier successful key selection does not consult later roles. Repeated components are allowed but excluded.

Eligible standalone blocks resolve as single. CC-15 keeps its base/default lease-up scenario exception; roles cannot choose another scenario. Its selected schedule must still be eligible. Existing direct reads observe roles when present, with their role-free behavior preserved. CC-14 checks section presence. No arithmetic, tolerance, tranche aggregation or scenario averaging changes.

If resolution fails, record variant_unresolvable coverage and emit CC-16 once per section, naming affected checks in sorted order. Role-bearing diagnostics state the collision or eligibility failure; role-free messages remain unchanged. A multi-leg check may evaluate another leg, so this issue does not imply that every affected check was wholly skipped.

Role selection evidence (RFC 0040). Optional coverage resolutions maps section ids to { variant?, via }, where via is preference, role, primary, default, base, sole, or single. Emit it only for selections from sections bearing roles, even when a later field check skips comparison. Multi-section checks record selections separately. Role-free coverage has no new fields. Constants, reporting and diagnostics do not alter financial math.

Coverage (RFC 0037). A conforming validator MUST report, alongside its issues, a coverage record for every CC-NN check it registers (CROSS_CHECK_RULE_IDS): evaluated when the comparison was actually performed, whether or not it produced an issue; or skipped with one of four reasons — section_absent, variant_unresolvable, field_absent, not_applicable — and a short detail naming the section, field or precondition. A check with no coverage entry is a check the validator does not implement. Coverage is reporting, not a verdict: it never changes overall_status. It exists because a document whose reconciliation checks all no-opped is indistinguishable from a reconciled one without it.

Asset-class identifier codes (RFC 0003). These sit outside the CC-NN sequence deliberately: they are about the identifier, not a disagreement between two sections, and grouping them with cross-section consistency checks would misfile them for anyone filtering by prefix.

CodeSeverityRule
INVALID-ASSET-CLASS-001errorasset_class is neither a builtin nor a well-formed namespaced identifier (§2.2a).
INVALID-ASSET-CLASS-002errorA namespaced identifier whose final segment shadows a builtin name (§2.2a).
MOD-DEPENDENCY-UNDECLAREDwarningA namespaced asset_class with no frontmatter modules list naming what declares it (§2.2a).

Resolution-time findings — MOD-FALLBACK-001, MOD-MISSING-001, MOD-ASSET-CLASS-CONFLICT-001, MOD-DISPLAY-CONFLICT-001 — are not validation rules and do not appear here. They depend on which modules a particular host has loaded, so the same document would validate differently for different readers; they belong to Protocol §X.2.2 and are reported by resolution, not validation.

CC-13 severity and applicability. CC-13 is a warning, never an error: a screening-stage deal legitimately does not know its RSF yet, and the gaps / provisional machinery already owns "we don't know this yet". An institution wanting a hard gate expresses it through INCOMPLETE_DATA_POLICIES. The rule fires only when all of the following hold:

  1. The source is a UWX record, not a UW Lite summary (Lite states size in its own grammar).
  2. The document's profile is a deal record (market-data and other non-deal profiles have no property section by construction).
  3. The asset class is recognized (Protocol §XIII.3) and is not mixed_use (Protocol §XIII.2).
  4. The document has a property section. A missing section is a different defect with a different remedy, and CC-13 would give the wrong diagnostic.
  5. The property section is not externalized (RFC 0021); the resolved record is where the field lives.

CC-14 severity and applicability. CC-14 is a warning, never an error: the RFC 0028 corpus scan found 28 documents a refusal would invalidate retroactively, and an institution wanting a hard gate expresses it through INCOMPLETE_DATA_POLICIES. The rule fires only when all of the following hold:

  1. The source is a UWX record, not a UW Lite summary (Lite states its property facts in its own grammar).
  2. The document's profile is a deal record (market-data and other non-deal profiles have no property section by construction).
  3. The property section is not merely externalized (RFC 0021) — an externalized-but-unresolved section is present, not missing.

CC-14 is unconditional on deal_stage — §4.1 requires the section at every stage. When CC-14 fires, CC-13 must not also fire (its precondition 4 ensures this), and DQ-06 must suppress its own property entry: one defect, one diagnostic.

CC-15 severity and applicability. CC-15 is a warning, never an error, and is tolerance-checked, never exact — see §4.25 for why the 2% band is deliberate. It reads only the base variant of lease_up_schedule (falling back to the sole/default variant when no base exists) and fires only when both stabilized_summary.annualized_noi and noi_model.net_operating_income are stated. Non-base variants are exempt by design: a downside scenario is supposed to disagree with stabilized NOI. stabilized_summary.annualized_noi deliberately does not also cross-check quick_metrics.noi_underwritten — quick_metrics is a frontmatter restatement with no cross-check of its own (CC-01 compares the rent roll to the operating statement, not NOI to the frontmatter), and RFC 0008 adds one seam, the trajectory endpoint against the stabilized-year model, not a second.


Part VI — Toolchain Interface ​

The uwmd command-line tool is the reference implementation. Other tools (AI agents, API consumers, rendering pipelines) implement a subset of this interface.

6.1 uwmd parse <file> ​

Reads a .uwx.md file and outputs a structured JSON object containing:

  • Frontmatter (as-is)
  • All non-superseded data blocks, keyed by section ID and variant
  • Pipeline log entries
json
{
  "frontmatter": { "...": "YAML frontmatter as JSON" },
  "sections": {
    "property": { "...": "most recent non-superseded block" },
    "operating_statement": {
      "t12": { "...": "t12 variant" },
      "t3": { "...": "t3 variant" }
    }
  },
  "prose": {
    "property": "string — the markdown prose block for this section",
    "...": "..."
  },
  "pipeline_log": [ "...entries..." ],
  "superseded_blocks": { "...": "all superseded blocks, keyed by section + version" }
}

6.2 uwmd validate <file> [--stage <stage>] ​

Runs all validation rules (§5) and returns:

json
{
  "overall_status": "clean | warnings | errors | blocking",
  "stage_readiness": {
    "screening": true,
    "full_underwrite": false,
    "credit_approval": false,
    "closing": false
  },
  "issues": [ "...ValidationIssue objects..." ]
}

6.3 uwmd run <file> --agent <agent_id> ​

Invokes a Bancroft agent against the file. The agent:

  1. Reads the parsed file via uwmd parse
  2. Performs its analysis
  3. Returns a new data block (the section it writes)
  4. uwmd run appends the new block to the file, superseding the previous version if one exists
  5. Appends a pipeline_log entry

6.4 uwmd render <file> --format <format> [--tier <tier>] ​

Renders the file to an output format. Formats:

FormatDescriptionRequires
pdfLender-ready deal package PDFL7 sections complete
docxCredit memo Word documentL7 sections complete
jsonExtracted data only (no prose)Any stage
csvFlat metrics summaryAny stage
chatCompressed context for AI assistantAny stage
summaryOne-page deal summary markdownScreening+

The --tier screener flag renders only the Tier 1 sections (no Bancroft-generated sections). The --tier analyst flag renders the full package.

6.5 uwmd compact <file> ​

Removes all blocks where _meta.superseded === true. Preserves the pipeline log. Outputs the compacted file. Appends a compact entry to the pipeline log.

6.6 uwmd diff <file_a> <file_b> ​

Compares two .uwx.md files (or two versions of the same file at different timestamps) and outputs a structured diff showing which sections changed, what values changed, and which source produced the change.

6.7 uwmd init --scenario <scenario> --address <address> ​

Creates a new .uwx.md file with:

  • Populated frontmatter
  • All section headers in canonical order
  • Empty data block stubs (all fields null) for each section
  • An initial pipeline_log entry recording creation

6.8 Agent Contract ​

Any Bancroft agent writing to a .uwx.md file MUST:

  1. Read the current state of its input sections before running
  2. Produce a single data block JSON object matching the section schema exactly
  3. Include a valid _meta object as the first key
  4. Set _meta.version to previous_version + 1 (or 1 if no previous block exists)
  5. Set _meta.superseded = false
  6. Never write to sections outside its defined scope (see §4 "Written by" field)
  7. Never set human_review_required: false if the block contains any low confidence values
  8. Return { "status": "awaiting_input", "required": ["section_id", ...] } if required input sections are missing, rather than failing

Part VII — Rendering Targets ​

7.1 Lender Package PDF (Tier 1) ​

Sections rendered, in order:

  1. Cover page (property photo, address, date, preparer)
  2. Executive Summary (key metrics table from quick_metrics)
  3. Property Overview → property section prose + formatted data
  4. Proforma / Cash Flow → noi_model formatted table
  5. Rent Roll Summary → rent_roll unit_mix_summary table
  6. Debt Structure → debt_structure terms table
  7. Sources & Uses → sources_uses capital stack
  8. Borrower Summary → borrower_sponsor.sponsor_narrative + financial summary
  9. Exit Analysis → dcf.exit_analysis + returns table
  10. Assumptions & Disclosures → assumptions table with source badges; standard disclaimer

7.2 Credit Memo (Tier 2 Analyst) ​

All Tier 1 sections plus:

  • Market Analysis → market_analysis full narrative + comp tables
  • Financial Analysis → Full DCF table + stress_tests matrix
  • Due Diligence Summary → due_diligence checklist and findings
  • Risk Assessment → risk_assessment component scores + narrative
  • Compliance Summary → compliance key findings
  • Covenants → debt_structure.covenants formatted table
  • Appendices → Raw agent outputs, full rent roll

7.3 AI Chat Context (--format chat) ​

A compressed representation of the deal for dropping into an AI assistant. Includes:

  • Full frontmatter
  • Prose sections (not data blocks — AI reads narrative faster)
  • Key metrics table from quick_metrics
  • Active flags from validation
  • Assumptions summary (by source category counts)

Omits: full unit-level rent roll data, full operating statement line items, pipeline log (included as a one-line summary instead).

Target token count: < 8,000 tokens for Tier 1 deal, < 16,000 for full Tier 2 deal.


Appendix A — File Naming Convention ​

{deal_id}_{address_slug}_{YYYYMMDD}.uwx.md

Example:

uw_2026_a3f9b1_1234-main-st-phoenix-az_20260424.uwx.md

Versioned snapshots (before compact):

uw_2026_a3f9b1_1234-main-st-phoenix-az_20260424_v3.uwx.md

Appendix B — Scenario Default Keys ​

The following keys MUST exist in the assumptions section for every deal, populated from scenario_defaults.json when no higher-authority source provides them:

vacancy_rate
rent_growth_rate_yr1
rent_growth_rate_yr2_plus
expense_growth_rate
management_fee_pct
replacement_reserves_per_unit
exit_cap_rate_spread_bps
hold_period_years
disposition_costs_pct
io_period_months
leverage_ratio_default
interest_rate_assumption
amortization_years

Appendix C — Extension & Customization Points ​

The format has four layers of extensibility, in order from most structured to least:

C.1 — Standard Section Extension Fields ​

Every standard section accepts two universal extension fields without any schema modification:

  • _notes (string) — Free-text annotation on any data block. Never validated, never consumed by the engine, always preserved. For human-to-human and human-to-AI annotation.
  • extensions (object) — A free-form key-value bag inside any standard section's data block for fields that don't fit the schema but are too closely related to warrant a separate section. Keys prefixed with x_ in extensions are never validated. Keys without x_ are reserved for future spec fields and will generate a warning from validators if unrecognized.

Example: a multifamily deal with a cell tower lease on the roof can store "extensions": { "x_cell_tower_rent_annual": 18000, "x_cell_tower_lease_expiration": "2031-06-30" } in the rent_roll block rather than creating a full extension section.

C.2 — Custom Calculations (§ 4.19) ​

For supplemental metrics, deal-specific ratios, and derived figures. Fully structured, supports live references to standard section values, result is stored and reproducible. See §4.19 for full spec.

C.3 — Custom Scenarios (§ 4.20) ​

For what-if analyses beyond the standard stress test matrix. Override any assumption by key; engine recomputes affected metrics. Supports single-variable sensitivity sweeps. See §4.20 for full spec.

C.4 — Extension Sections (§ 4.21) ​

For content that doesn't fit any structured framework — renovation plans, lender outreach trackers, partnership structures, negotiation logs. Identified by x_ prefix on section ID. No schema enforcement; parsers collect without error; renders include in appendix when flagged. See §4.21 for full spec and documented community patterns.

C.5 — Custom Assumption Sources ​

Any source string not in the defined list (§2.6) is treated as "custom" by validators. It is recorded, not rejected. Useful for proprietary data feeds, internal systems, or third-party integrations.

json
{ "source": "custom:argus_export_2026_q1" }

C.6 — Custom Validation Thresholds ​

The .uw.institution.json sidecar file (same directory as the .uwx.md file) can override the default validation thresholds from §5.2 on a per-institution or per-user basis:

json
{
  "institution_id": "string",
  "validation_overrides": {
    "dscr_warning_below": 1.25,
    "ltv_warning_above": 0.70,
    "debt_yield_warning_below": 0.09
  },
  "required_sections_overrides": {},
  "render_template_overrides": {}
}

When a .uw.institution.json is present, its thresholds take precedence over the spec defaults. This allows institutional deployments to enforce their own credit policy without modifying the spec.

C.7 — Spec Evolution Protocol ​

Extension sections that stabilize into consistent schemas are candidates for promotion to standard sections. Promotion criteria:

  1. The x_ section has been used across ≥ 5 distinct deals with a consistent schema
  2. The schema_version field is set to a semver string (signals author believes it's ready)
  3. A spec change proposal is submitted with: the schema, use-case description, 3+ example instances, and proposed standard section ID

Promoted sections are assigned a section number in the next minor version of the spec. The x_ version continues to parse correctly — old files are never broken.


Appendix D — YAML Subset (Frontmatter) ​

The .uwx.md frontmatter (between the opening and closing --- markers) is parsed against a strict YAML subset, not full YAML 1.2. Conforming readers MUST reject frontmatter that uses any feature outside this subset with the validator code UNSUPPORTED_YAML_FEATURE.

D.1 — Supported ​

FeatureExample
Scalar — bare stringstate: AZ
Scalar — single-quoted stringnotes: 'I need to recheck'
Scalar — double-quoted stringdeal_name: "Parkview Apartments"
Scalar — integer or decimal numberpurchase_price: 7200000
Scalar — booleanhuman_review_required: true
Scalar — nullextension_id: null (or ~)
Empty inline sequenceflags: []
Mapping (top level)key: value per line
Mapping (nested, one level)indented two spaces under a key: line
Sequencedash-prefixed items under a key: line
Comments# comment to end of line

D.2 — Rejected ​

The following YAML features are NOT part of the subset and MUST cause the parser to throw UNSUPPORTED_YAML_FEATURE:

FeatureWhy rejected
Anchors and aliases (&name, *name)Implicit cross-references conflict with the explicit-provenance design; surprising round-trip behavior in editors.
Explicit tags (!!str, !!int, !Custom)Type coercion is governed by the spec's field schemas, not YAML tag dispatch.
Block scalars (|, >)Multi-line literal scalars create ambiguous indentation rules and cause silent data loss when concatenated.
Flow-style mappings or non-empty sequences ({a: 1}, [1, 2, 3])The on-disk representation is meant to be diff-friendly and one-value-per-line. The empty inline sequence [] is the lone exception, since it is unambiguous.
Complex keys (? indicator)Mapping keys must be simple scalars in .uwx.md.
Directives (%YAML, %TAG)The format pins a single YAML dialect (this subset).

D.3 — Rationale ​

The format is intentionally minimal so a reader can be implemented in a few hundred lines without depending on a full YAML parser. Banning these features early — rather than allowing them and quietly producing wrong data — keeps cross-implementation behavior identical and prevents subtle round-trip bugs in editors that re-emit frontmatter.

If you need long-form prose in a section, put it in the markdown body (between fenced section blocks) or in a dedicated extension section, not in frontmatter.


Specification version 1.1 | underwriter.cc | April 2026
v1.0 → v1.1: Added §4.0 (Deal Context), §4.19 (Custom Calculations), §4.20 (Custom Scenarios), §4.21 (Extension Sections); expanded Appendix C; added universal _notes field and reference path notation.
This document is itself a reference artifact. The canonical format for a deal is a .uwx.md file. This spec defines what valid means.

Markdown opt-in. When an active section declares _role and has explicit variant= fences, readers collect all its active named variants, even outside the legacy multi-variant section registry. Collect before choosing a version, so fence order cannot discard an unannotated peer. Superseded roles do not activate selection. Role-free Markdown retains its existing routing behavior; removing a section's last role ends this opt-in. UW JSON envelopes already represent variant maps on every section.

Released under the MIT License.