Status: implemented
Implemented — this change has shipped.
RFC 0069: Declare the student rent-roll bed counts and pre-leasing dates the student pack already reads
Implemented; released in core/CLI 2.16.0 with Protocol 2.20.0 and Format 2.0. On 2026-10-02 the maintainers accepted this RFC as a whole and authorized implementation. See Resolution.
Drafted with AI assistance; reviewed by maintainers. An adopter's student-housing export and its issue report are requirements evidence, not UWMD authority or owner authorship.
Three accepted decisions of 2026-10-01 settle specific validation semantics:
- a stated bed count may not exceed
property.total_beds; - a stated
preleased_bedsrequires both pre-leasing dates; - a stated
occupied_bedsrequires the roll'sas_of_date.
Acceptance resolved the draft's four open questions; see Resolved questions:
- senior housing is out of scope;
student_housingcomponents of amixed_usedeal are out of scope;- another class that states the fields draws no
infoissue, but theBEDrules still apply to what it states; - future capacity is deferred, and
BED-02is not weakened for it.
The accepted field contract:
occupied_bedsis an OPTIONAL nonnegative integer, measured on a realrent_roll.as_of_date.preleased_bedsis an OPTIONAL nonnegative integer. It requires a realpreleased_as_ofand a realpreleased_term_start, andpreleased_as_of < preleased_term_start.- Neither count may exceed
property.total_beds. - The counts are stated, never derived or cross-inferred.
- An absent count produces no
BEDissue, and its pack metric staysnull. - One
BED-NNerror family, as drafted. - No pack formula changes.
Summary
org.uwmd.pack.student_housing computes occupancy as rent_roll.occupied_beds / property.total_beds and pre_lease_rate as rent_roll.preleased_beds / property.total_beds. The tier-3 fixture student-housing-pre-lease-rate states preleased_beds. The format spec declares neither field, and no field carries the date either count was measured on.
This RFC declares four OPTIONAL §4.3 roll-total fields for student_housing documents, and a registered BED-NN validator family:
- An absent count is not an issue. Incomplete screening data may omit the counts and the whole pre-leasing tuple.
- A stated count must be a complete, well-typed fact. It must be a nonnegative integer, within
property.total_beds, and dated: the roll'sas_of_dateforoccupied_beds, both pre-leasing dates forpreleased_beds. Every violation is an error.
A producer could then state the fields without copying a fixture. Pack formulas do not change.
Motivation
Re-measured on main at c1a807b (2026-10-01). References name files, symbols and spec sections rather than line numbers.
The fields are undeclared. Format §4.1 declares
property.total_bedsand nothing else about beds. Its field notes calltotal_bedsthe primary size field forstudent_housingand a secondary forsenior_housing, and say both classes carry two counts (RFC 0027). The §4.3rent_rollMultifamily Variant template, which astudent_housingroll uses (rent_roll_type: multifamily), statesas_of_dateand unit totals (total_unitsthroughmonth_to_month_pct) only. No file underspec/mentionsoccupied_beds,preleased_bedsor a pre-leasing date.The pack, fixture and example already use them.
- In
packages/uwmd-core/src/packs/student-housing.ts, theoccupancyandpre_lease_ratecalculations readrent_roll.occupied_bedsandrent_roll.preleased_beds. conformance/tier-3-calc-host/fixtures/student-housing-pre-lease-rate/statesproperty.total_beds: 600andrent_roll.preleased_beds: 570, and expects0.95.- The worked example
examples/Mill-Ave-Commons-Student-Tempe-AZ.uwx.mdstatesoccupied_beds: 567andpreleased_beds: 573in its roll. It has noas_of_dateand no date for the pre-leased count. Its roll also states fields the template does not declare: arent_roll.total_bedsthat repeatsproperty.total_beds, storedoccupancyandpre_lease_rateratios, and aunit_mixarray with per-floor-planbedsandoccupied_beds(the template's key isunit_mix_summary). - The calc-pack wiki (
docs/wiki/05-calc-packs.md,STUDENT_HOUSING_PACK) says the two counts are deliberately separate because they are measured on different dates: next academic year's pre-leasing against today's in-place occupancy.student-housing.test.tsasserts that they differ.
- In
The consequence. A conforming producer that states only declared fields cannot make
occupancyorpre_lease_rateevaluate. A producer that copies the fixture states undeclared fields with no type, bound or measurement date. An adopter reported this on 2026-09-28 while exporting a student-housing deal.No family to extend. The validator has no rent-roll code family. The roll-adjacent rules today are:
CC-01, a cross-section check;REC-NN, expense recoveries (RFC 0058);LSE-NN, lease clauses (RFC 0055).
The last two are each owned by the feature that introduced them. Protocol §III.6a is the registered-family table.
npm run verify-codesfails when an emitted family is not registered there, or when a rule bullet in the format spec is not emitted.
Proposed change
This section is the accepted contract, as written into the specs.
Format spec §4.3 (normative)
Template. In the Multifamily Variant template of spec/UW_FORMAT_SPEC_v1.md §4.3, insert four keys after "month_to_month_pct" and before "units". They default to null because, like the §4.1 size fields, a count that does not apply is an absence and not a zero:
"month_to_month_pct": 0.0,
"occupied_beds": null,
"preleased_beds": null,
"preleased_as_of": null,
"preleased_term_start": null,
"units": [Prose. Insert the following after the Multifamily Variant template and before #### Commercial Variant, in the style of the RFC 0058 "Expense recoveries and the CAM true-up" paragraph:
Student-housing bed counts (RFC 0069).
occupied_beds,preleased_beds,preleased_as_ofandpreleased_term_startare OPTIONAL. They are meaningful whenasset_classisstudent_housing; other classes SHOULD leave themnullor absent. Student housing leases by the bed and re-leases nearly its whole roll on one date, so the roll carries two bed counts measured on different dates.
occupied_beds(integer, ≥ 0) — beds under a lease in place as of the roll'sas_of_date.preleased_beds(integer, ≥ 0) — beds under a signed lease for the academic term that begins onpreleased_term_start, counted as ofpreleased_as_of.preleased_as_of(YYYY-MM-DD) — the datepreleased_bedswas measured.preleased_term_start(YYYY-MM-DD) — the first day of the termpreleased_bedsdescribes. The term is in the future relative topreleased_as_of, not relative to the date a tool reads the file.The fields describe the top-level
rent_rollonly. They do not extend tosenior_housing, which also statestotal_beds(§4.1), and they give astudent_housingcomponent of amixed_usedeal (§4.23) no bed-count semantics. A class other thanstudent_housingthat states them draws no issue for doing so; the rules below still apply to what it states.The fields are optional as a group. A roll may omit either count, and with
preleased_bedsthe whole pre-leasing tuple. A count that is stated is a complete typed fact only with its measurement date:
- A stated
occupied_bedsMUST be accompanied by the roll'sas_of_dateas a real date.- A stated
preleased_bedsMUST be accompanied by bothpreleased_as_ofandpreleased_term_start.The two counts are stated, never derived. Pre-leasing never sets occupancy, neither count is inferred from the other, and no rule compares the two.
property.total_beds(§4.1) remains the size field and the denominator of every per-bed metric (Protocol §XIII.1). Neither count may exceed it, including for capacity a later phase will deliver; future-phase capacity needs its own explicit representation, which this section does not define.An absent count is not an issue. A roll that omits a count draws no
BED-*issue for it, and a pack metric over an absent count resolves tonull(Protocol §VIII.2). Stage requirements for absent facts belong to incomplete-data policies, not to this family. A pre-leasing date stated withoutpreleased_bedsis inert context: it must still be a real date, and the pair must still be in order, but it requires nothing else. Every rule compares stated values with each other, never with the current date or file metadata.
BED-01— error when a statedoccupied_bedsorpreleased_bedsis not a finite, nonnegative integer. This is checked whether or notproperty.total_bedsis stated.BED-02— error when a validoccupied_bedsorpreleased_bedsexceedsproperty.total_beds. The comparison runs only whenproperty.total_bedsis a finite number. Otherwise only this comparison is skipped;CC-13already reports an absent or non-numeric primary size field.BED-03— error whenoccupied_bedsis stated and the roll'sas_of_dateis absent or not a realYYYY-MM-DDdate.BED-04— error whenpreleased_bedsis stated andpreleased_as_oforpreleased_term_startis absent.BED-05— error when a statedpreleased_as_oforpreleased_term_startis not a realYYYY-MM-DDdate. This is checked whenever the date is stated, with or withoutpreleased_beds.BED-06— error when both pre-leasing dates are real andpreleased_as_ofis on or afterpreleased_term_start. A count measured on or after the day the term began describes occupancy, not pre-leasing.
Why each condition is treated this way
| Condition | Rule | Basis |
|---|---|---|
| A count is absent because underwriting is incomplete | No BED-* issue; the metric resolves to null | New §4.3 fields are OPTIONAL and absence is unchanged ("a tenant stating none of them is unchanged", RFC 0055/0058). Stage gating of absent data is IncompleteDataPolicy (halt, degrade, substitute, defer per section, field and stage); its built-in rent_roll entries already degrade at screening and halt at full underwrite. |
| A stated count is non-numeric, non-integer, non-finite or negative | BED-01 error | Feature families refuse a stated value that breaks its typed contract: REC-01 (fractions out of range), LSE-09 (amounts finite and nonnegative). |
A valid count exceeds property.total_beds | BED-02 error, for either count | Accepted decision 1. property.total_beds is the normative student-housing size denominator the pack uses. A future-phase capacity is not admitted through a warning; representing it needs its own explicit contract. In-family bounds are errors elsewhere too (LSE-09, REC-07). |
occupied_beds stated without a real roll as_of_date | BED-03 error | Accepted decision 3. occupied_beds is defined as measured on as_of_date, so a count without it is not a complete typed fact. A malformed as_of_date leaves the count just as undated, so it falls under the same rule. No BED-* rule checks as_of_date when occupied_beds is absent; that remains a general roll field. |
preleased_beds stated, a pre-leasing date absent | BED-04 error | Accepted decision 2. MUST plus error, as LSE-03, LSE-06 and LSE-08 treat required companion fields. The tuple as a whole stays optional. |
A stated pre-leasing date is not a real YYYY-MM-DD date | BED-05 error | REC-04, LSE-01, LSE-04 and CAPX-01 refuse a stated non-date. |
preleased_as_of ≥ preleased_term_start | BED-06 error | Date-order contracts are errors: REC-05 (a true-up ends strictly before the roll's as_of_date) and LSE-02 (steps inside the lease term). |
A pre-leasing date stated without preleased_beds | No requirement beyond BED-05/BED-06 | Inert context. Nothing in the existing contract requires a count to accompany a date, and the pack reads only the counts. No rule is added for symmetry. |
Protocol §III.6a (normative)
Add one row to the registered-family table:
| Prefix | Family | Owning capability | Default severity |
|---|---|---|---|
BED-NN | Student-housing bed counts — in-place and pre-leased beds and the dates they were measured on (format §4.3, RFC 0069). Counts are stated, never derived from one another. | validate | error |
One family. A feature-scoped family is how §III.6a registers rule sets such as REC-NN, LSE-NN and CAPX-NN. Every BED-NN code is an error, because each one refuses a stated value that is malformed, out of bounds, or missing its required companion. Absence of a count emits nothing, so the family never reports incomplete underwriting.
Library
@uwmd/core gains six validator rules (BED-01..BED-06), emitted from packages/uwmd-core/src/validator.ts beside the other feature families. Each issue's message is its description, as with REC-NN and LSE-NN; there is no separate description registry in protocol.ts.
The change is additive: no export changes shape, no pack formula changes, and STUDENT_HOUSING_PACK is untouched. UW_FORMAT_SPEC_v2.md is a delta specification that incorporates v1 Part IV by reference, so the v1 edit governs 2.0 files without a second edit. No JSON Schema declares rent_roll, so none changes.
Because verify-codes fails a format-spec rule bullet that nothing emits, the §4.3 bullets and the validator rules must land in the same change.
Compatibility analysis
- Existing
.uw.mdfiles — absent fields draw nothing, so a document that never states a count is unaffected. A document that states a count must now state it completely:- The Mill Ave example states both counts as integers within
total_beds, but its roll has noas_of_dateand no pre-leasing dates. It would drawBED-03andBED-04errors, so the implementation adds those dates to it in the same change. - The tier-3 fixture
student-housing-pre-lease-ratestatespreleased_bedswithout dates. Tier-3 cases evaluate the calculation only, so its expected result does not move. The implementation still adds the dates, so the corpus never shows a count the validator would refuse. - Any other document stating these previously undeclared names without their dates, or with a malformed value, would gain errors. None is known in the corpus. The adopter export would need to state the dates alongside the counts, or omit the counts.
- The Mill Ave example states both counts as integers within
- Tier-1 Reader — unaffected. The parser already carries undeclared content keys, which is how the pack reads these fields today. Rendered summaries are unchanged unless a renderer chooses to show the counts.
- Tier-2 Editor — four more editable fields; no edit-semantics change.
- Tier-3 Calc Host — no formula changes.
occupancyandpre_lease_rateevaluate exactly as they do today; the draft declares the inputs they already read. ABED-*error is a validation result and does not change calc evaluation. - Tier-4 Agent Host — none.
- Modules — no manifest schema change;
STUDENT_HOUSING_PACKis unchanged. - Excel —
packages/uwmd-excel/src/student-housing.tsalready carriesoccupied_bedsandpreleased_bedsasNamedInputs (format: 'count').NamedInput.formatis'currency' | 'count'(layout.ts), so the dates have no workbook representation and the layout does not change. Exposing dates as workbook inputs is a separate, demand-gated change. Excel ↔ calc parity is unaffected because no formula or input path changes.
No deprecation path is needed: the fields were never declared, so no conforming document relied on an undated count.
Conformance impact
Existing files the implementation updates in the same change:
conformance/tier-3-calc-host/fixtures/student-housing-pre-lease-rate/deal.uwx.mdgainspreleased_as_ofandpreleased_term_startbesidepreleased_beds.calc.jsonandexpected-result.jsonare unchanged (0.95).examples/Mill-Ave-Commons-Student-Tempe-AZ.uwx.mdgainsas_of_date,preleased_as_ofandpreleased_term_startin its roll. The pack test that reads it keeps passing because the counts do not move. The example's other undeclared roll fields are outside this RFC and are neither declared nor read here.
New tier-1 reader fixtures (next free numbers 14 and 15), each with the four standard baselines (.parsed.json, .rendered-chat.txt, .rendered-summary.md, .validation.json):
14-student-bed-counts.uwx.md— astudent_housingdeal whose roll states everything consistently:as_of_date: 2026-03-15,total_beds: 600,occupied_beds: 567,preleased_beds: 573,preleased_as_of: 2026-03-15,preleased_term_start: 2026-08-15. Baseline: noBED-*issue.15-student-bed-counts-inconsistent.uwx.md— the same roll withpreleased_beds: 620overtotal_beds: 600andpreleased_as_ofafterpreleased_term_start. Baseline:BED-02andBED-06errors, and nothing for the valid, datedoccupied_beds.
Unit tests pin each condition separately:
BED-01. A string, a fraction, a negative and a non-finite count each error, including whenproperty.total_bedsis absent. That absence skips onlyBED-02.BED-02. Each count at, above and below the total; skipped whentotal_bedsis absent or non-numeric.BED-03.occupied_bedswith noas_of_date, and with a malformed one; no issue for a malformedas_of_datewhenoccupied_bedsis absent.BED-04.preleased_bedsmissing each date in turn, and both.BED-05. A malformed pre-leasing date stated withpreleased_beds, and without it.BED-06. Equality and later, with and withoutpreleased_beds; noBED-06when either date is invalid, sinceBED-05covers that.- Inert dates. Valid, ordered pre-leasing dates with no
preleased_bedsdraw no issue. - Absent counts. No
BED-*issue for a roll stating none of the fields, andoccupancyandpre_lease_rateevaluate tonull(Protocol §VIII.2). No issue for a non-student class that states nothing, or states valid, dated counts. - No cross-comparison. No rule ever compares
occupied_bedswithpreleased_beds, or either with the current date.
Reference implementation
Implemented in the change that records this acceptance.
Files affected:
spec/UW_FORMAT_SPEC_v1.md(§4.3 template and prose);spec/UW_PROTOCOL_v1.md(§III.6a row; version label);packages/uwmd-core/src/validator.ts(checkStudentBedCounts);packages/uwmd-core/src/protocol.ts(PROTOCOL_VERSION);- the fixture, the example and the two new tier-1 fixtures listed above;
docs/wiki/05-calc-packs.mdand the living records.
checkStudentBedCounts reads the property-level rent_roll. It selects the block with RFC 0040 roles, the same selection the lease-clause rules use, so a component block is never the roll it checks. It reads the block's payload the way the calc evaluator does (blockPayload, Protocol §VIII.2), so a v1 flat block and a v2 content envelope are judged alike. property.total_beds is read from the property block CC-13 reads.
API surface: none. No new export and no type change.
Version. The change adds optional §4.3 fields and registers a validator family, the same additive shape RFC 0058 shipped as a Protocol minor. It therefore takes Protocol minor 2.20.0, released in core/CLI 2.16.0 and now stable. Format stays 2.0: UW_FORMAT_SPEC_v2.md incorporates v1 Part IV by reference, as for RFCs 0055 and 0058. The version follows from existing semver-per-surface precedent; it is not a separate accepted decision.
Separate concern, not in scope. CC-13 accepts any finite number as a class's primary size field, so it does not enforce that total_beds is a nonnegative integer. That is a pre-existing size-intensive validation concern. BED-02 compares a valid count with whatever finite total_beds is stated, and stays correct as specified, so this RFC does not change CC-13.
Test plan:
- the unit tests above;
- the two new tier-1 fixtures with baselines;
- the existing
student-housing.test.ts, unchanged; npm run verify-codes, with the new family registered and all six codes emitted;npm run conformance.
Alternatives considered
- Per-floor-plan bed columns in the roll detail. A
bedsarray besideunits, with per-type occupied and pre-leased counts. The Mill Ave example already carries an undeclared per-typeunit_mix, so there is some producer shape to learn from. But it is a larger schema change and the pack reads only totals. Deferred. - A single
pre_leaseobject ({ beds, as_of, term_start }). Keeps the three pre-leasing facts together. But the pack already reads the flat pathrent_roll.preleased_beds. Separate top-level fields match the flat roll-totals shape the template already uses and keep the pack formulas' paths unchanged.BED-04gives the flat fields the same all-or-nothing completeness an object would. - Storing the ratios (
occupancy,pre_lease_rate) on the roll instead of the counts. The Mill Ave example does this today beside the counts. The pack computes the ratios deterministically. Storing them invites disagreement with no check to catch it, which is the failure mode the calc engine exists to prevent. - A warning for a pre-leased count above
total_beds, to allow for a phase delivering before the coming term. Rejected by accepted decision 1:property.total_bedsis the denominator, and future capacity needs its own explicit contract rather than a warning. - SHOULD plus a warning for missing pre-leasing dates, as in an earlier revision of this draft. Rejected by accepted decision 2: an undated pre-leased count is not a complete fact, and the tuple's optionality already covers incomplete screening data.
- No date requirement for
occupied_beds. Rejected by accepted decision 3: the count is defined as measured on the roll'sas_of_date. - All warnings. This was the original draft's choice. It would let a malformed typed value (a negative or fractional bed count, a non-date) through as advisory, unlike every other feature family's typed-field rules, and it paired RFC 2119 MUST with a warning.
- A count required whenever a pre-leasing date is stated. Not adopted. Nothing in the existing contract requires it, the pack reads only the counts, and a rule added only for symmetry would refuse harmless context.
- Extending an existing family (
CC-NN,LSE-NN,REC-NN).CC-NNis for two sections disagreeing about one fact, and the others are owned by their features. A new feature-scoped family is how §III.6a has registered rule sets since RFC 0008 (LU-NN).
Resolved questions
The maintainers resolved the draft's four open questions on acceptance, 2026-10-02:
- Senior housing: out of scope.
senior_housingalso statestotal_bedsas a secondary size (§4.1), but RFC 0069 does not extendoccupied_beds,preleased_bedsor the pre-leasing date contract to it. - Mixed-use components: out of scope. RFC 0069 governs the top-level student-housing
rent_roll. It defines no bed-count semantics for astudent_housingcomponent of amixed_usedeal (§4.23). If adopter evidence later requires them, they need a separate contract. - Other classes stating the fields: the draft behavior stands. A non-student class that states the fields draws no
infoissue for doing so. When the fields are stated, theBEDtyped and date rules still apply. - Future capacity: deferred.
BED-02is not weakened for planned future beds. Future-phase capacity requires its own explicit representation.
Resolution
RFC 0069 was accepted on 2026-10-02 and is implemented, released in core/CLI 2.16.0 with Protocol 2.20.0 and Format 2.0.
| Item | Source | State |
|---|---|---|
property.total_beds is the student-housing size field and per-bed denominator | Format §4.1, Protocol §XIII.1 (RFC 0027) | Existing normative rule. Unchanged. |
Pack formulas for occupancy and pre_lease_rate | STUDENT_HOUSING_PACK | Existing. Unchanged. |
A stated occupied_beds or preleased_beds may not exceed property.total_beds; both are errors (BED-02); no future-phase exception through a warning | Accepted decision 1, 2026-10-01 | Decided. Future capacity needs its own contract. |
A stated preleased_beds requires preleased_as_of and preleased_term_start: MUST plus error (BED-04); the tuple stays optional | Accepted decision 2, 2026-10-01 | Decided. |
A stated occupied_beds requires the roll's as_of_date as a real YYYY-MM-DD date; an absent or malformed (non-real) as_of_date is an error (BED-03) | Accepted decision 3, 2026-10-01, confirmed by the UWMD maintainers to cover both an absent and a malformed as_of_date | Decided. |
Acceptance of RFC 0069 as a whole: field names, types and date semantics; one BED-NN error family; absent counts draw nothing; dates without a count are inert; no pack formula change | Owner acceptance, 2026-10-02 | Accepted. Format §4.3, Protocol §III.6a. |
Senior housing, mixed_use components, other classes, future capacity | Owner scope resolutions, 2026-10-02 | Resolved; see Resolved questions. |
| Protocol 2.20.0; Format unchanged at 2.0 | Semver-per-surface precedent (RFC 0058) | Consequence of acceptance, not a separate accepted decision. |
| Implementation | Owner authorization, 2026-10-02 | Released in core/CLI 2.16.0; RFC status implemented after verification. |
Prior art
- RFC 0027 (asset-class size intensives) declared
total_beds, and Protocol §XIII.1 made it the student-housing denominator. This RFC completes the roll-side pair the pack assumed. - RFC 0055 and RFC 0058 are the pattern for optional §4.3 fields with a feature-scoped family, a §III.6a row and typed-contract errors.
- Student-housing operators commonly report pre-leasing as a share of beds for the coming academic year as of a stated date. That is the pair
preleased_beds/preleased_as_ofrecords. No specific report is cited.