RFC 0006: Hospitality reference module
Summary
The protocol's module system has been specified (Part X) and a worked example is sketched in Appendix E, but no module has actually been built. This RFC commits to shipping @uwmd/module-hospitality as the reference implementation of a module — the worked example, fully realized — so that the module-system contract is exercised end-to-end and so that hospitality (hotels, motels, extended-stay) becomes a first-class supported asset class.
Motivation
Two reasons to do this now:
The module system has no consumer.
ModuleManifest,ModuleSectionDecl,ModuleCalcDecl, etc., are all defined in the protocol surface but never loaded by any code that ships. Until a real module exercises the loader, every assumption baked into the types is unverified.Hospitality is a real adopter ask. The asset class
hospitalityexists in the v1 enum, but the standard sections (property,noi_model, etc.) lack the fields hotel underwriting actually needs: ADR, occupancy, RevPAR, STR comps, F&B revenue, complimentary breakfast cost, brand fees. A hotel deal authored against the v1 standard sections has to stuff hotel-specific fields intonoi_model.other_incomeand lose the structure.
This RFC ships the module and uses the build process to discover any module-system bugs that need fixing in @uwmd/core first.
Proposed change
New package: @uwmd/module-hospitality
packages/uwmd-module-hospitality/
├── package.json
├── manifest.yaml # the canonical module manifest
├── manifest.json # generated from manifest.yaml at build time
├── src/
│ ├── index.ts # re-exports the manifest as a typed object
│ └── view-models.ts # SectionViewModel definitions for hotel sections
├── README.md
└── test/
└── fixtures/
└── boutique-hotel-austin.uw.mdManifest contents
manifest_version: "1"
id: org.uwmd.hospitality
name: Hospitality Underwriting Module
version: 0.1.0
description: |
Adds hotel-specific sections (operating metrics, brand fees, F&B),
RevPAR/ADR/occupancy calculations, and STR-comp validations.
authors: ["UW Markdown contributors"]
license: MIT
requires_protocol: ">=1.0.0 <2.0.0"
requires_format: ">=1.1 <2.0"
requires_tier: tier-3-calc-host
asset_classes: [hospitality]
sections:
- id: hotel_metrics
display_name: Hotel Operating Metrics
schema:
type: object
required: [adr, occupancy, available_room_nights, key_count]
properties:
adr: { type: number, minimum: 0 }
occupancy: { type: number, minimum: 0, maximum: 1 }
available_room_nights: { type: integer, minimum: 0 }
key_count: { type: integer, minimum: 0 }
market_revpar: { type: [number, "null"] }
required: true
- id: hotel_brand
display_name: Brand & Franchise
schema:
type: object
properties:
flag: { type: [string, "null"] }
franchise_fee_pct_of_rooms: { type: number, minimum: 0, maximum: 0.20 }
marketing_fund_pct: { type: number, minimum: 0, maximum: 0.10 }
loyalty_program_pct: { type: number, minimum: 0, maximum: 0.10 }
- id: hotel_food_beverage
display_name: Food & Beverage
schema:
type: object
properties:
fb_revenue: { type: number, minimum: 0 }
fb_cogs: { type: number, minimum: 0 }
fb_labor: { type: number, minimum: 0 }
complimentary_breakfast_cost_per_occupied_room: { type: number, minimum: 0 }
calculations:
- id: revpar
label: RevPAR
formula: hotel_metrics.adr * hotel_metrics.occupancy
unit: "$"
deterministic: true
- id: total_room_revenue
label: Total Room Revenue
formula: hotel_metrics.adr * hotel_metrics.occupancy * hotel_metrics.available_room_nights
unit: "$"
deterministic: true
- id: room_revenue_per_key
label: Room Revenue / Key
formula: total_room_revenue / hotel_metrics.key_count
unit: "$"
deterministic: true
- id: revpar_index
label: RevPAR Index vs Market
formula: revpar / hotel_metrics.market_revpar
unit: "x"
deterministic: true
- id: fb_gross_margin
label: F&B Gross Margin
formula: (hotel_food_beverage.fb_revenue - hotel_food_beverage.fb_cogs - hotel_food_beverage.fb_labor) / hotel_food_beverage.fb_revenue
unit: "%"
deterministic: true
validations:
- code: CC-MOD-HOSP-01
severity: warning
message: "RevPAR is more than 15% below market RevPAR — verify pricing strategy or comp set."
rule: hotel_metrics.market_revpar == null || revpar >= hotel_metrics.market_revpar * 0.85
- code: CC-MOD-HOSP-02
severity: error
message: "Occupancy must be between 0 and 1 (use a fraction, not a percentage)."
rule: hotel_metrics.occupancy >= 0 && hotel_metrics.occupancy <= 1
- code: CC-MOD-HOSP-03
severity: warning
message: "Total franchise fee burden exceeds 13% of room revenue — confirm flag economics."
rule: |
(hotel_brand.franchise_fee_pct_of_rooms + hotel_brand.marketing_fund_pct + hotel_brand.loyalty_program_pct) <= 0.13
view_models:
# Section-by-section layout for hotel_metrics, hotel_brand, hotel_food_beverage.
# Defined in src/view-models.ts and re-exported through the manifest at build time.Loader changes in @uwmd/core
Most of the loader infrastructure exists as types but not as code:
loadModule(manifest: ModuleManifest, options): ModuleRegistry— registers sections, calcs, validations, view models.validateAgainstModule(parsed, registry): ValidationResult— runs both built-in validators and the module'svalidations.evaluateCalcalready accepts aModuleCalcDecl— needs an extension to resolveformulareferences that cross the module's declared sections (hotel_metrics.adr).
Test fixtures
packages/uwmd-module-hospitality/test/fixtures/boutique-hotel-austin.uw.md is a complete hotel deal: 80 keys, $180 ADR, 72% occupancy, Marriott Autograph flag, full F&B program. The test suite:
- Loads the manifest into a registry.
- Parses the fixture file, which references the module via frontmatter.
- Validates: expects no errors, two warnings (intentional — fixture has slight RevPAR underperformance and franchise burden right at the threshold to cover both branches).
- Evaluates each of the five module calcs.
- Asserts the
revpar,total_room_revenue,room_revenue_per_key,revpar_index, andfb_gross_marginresults match expected values within tolerance.
Compatibility analysis
- Existing
.uw.mdfiles — no impact. Files that don't load this module continue to work. - Existing implementers — Tier-1 readers without module support continue to render hotel files using the standard sections; the hotel-specific sections render as raw JSON (existing fallback behavior). Tier-2 editors preserve hotel sections on round-trip via the standard mechanism. Tier-3 calc hosts that don't load the module skip the calcs.
- Spec changes — none. This RFC is a use of the existing module system.
Conformance impact
New fixtures in conformance/tier-3-calc-host/fixtures/:
module-hospitality-revpar/— uses the hospitality module, computes RevPAR, expected180 * 0.72 = 129.60.module-hospitality-validation-comp/— file withrevpar < market_revpar * 0.85, expectedCC-MOD-HOSP-01warning.
New fixtures in conformance/tier-2-editor/fixtures/:
module-hospitality-section-replace/— round-trips a hotelnoi_modelchange without touching the hotel-specific sections.
The fixture file itself (boutique-hotel-austin.uw.md) ships in examples/ so external readers have a real hotel example to inspect.
Reference implementation
- Files affected:
packages/uwmd-module-hospitality/(new package).packages/uwmd-core/src/module-loader.ts(new) —loadModule,ModuleRegistry.packages/uwmd-core/src/validator.ts— invoke module validators.packages/uwmd-core/src/calc/evaluator.ts— resolve module-section paths in formulas.examples/boutique-hotel-austin.uw.md(new).- Conformance fixtures listed above.
- API surface:
loadModule,ModuleRegistry, plus the@uwmd/module-hospitalitypackage's exports. - Test plan: the fixture-driven tests above; plus negative tests (bad manifest, missing section, calc references undeclared section).
Alternatives considered
Don't ship a reference module; document how to write one. The status quo. Rejected — see Motivation; the module system has no consumer and the contract goes unverified.
Build hospitality as a sample inside
@uwmd/corerather than a separate package. Rejected — defeats the demonstration. The whole point is to show the module system works for an external module.Add hospitality fields to the standard schema rather than as a module. Rejected — bloats the standard for the 60% of adopters not in hospitality, and the same argument would apply to gas stations, life sciences, data centers, …
Ship multiple reference modules at once (hospitality, life-sciences, data-center). Rejected for v2.0 — one is enough to validate the contract; build the others against the proven contract afterwards.
Use an existing hotel-underwriting taxonomy (STR, HotStats). Considered. Their data dictionaries are excellent references; we draw vocabulary from them but ship our own normative schema rather than depend on a paid taxonomy.
Unresolved questions
- Manifest format. YAML or JSON canonical? Recommend ship YAML as the source of truth (more readable for module authors), generate JSON at build time for consumers and schema validation.
- View model definitions. The manifest's
view_modelsfield accepts a free-form structure. Recommend a typedSectionViewModel[]shape (already inprotocol.ts) and have the build step convert TypeScript exports into the JSON shape stored in the manifest. - Bundling. Should the manifest ship as part of
@uwmd/module-hospitality's npm package, or as a standalone JSON artifact? Recommend both — the npm package for TS consumers, adist/manifest.jsonthat any host can fetch independently. - Versioning vs the asset class. The asset class is
hospitality(built-in v1 enum). The module is@uwmd/[email protected]. If a future module also targetsasset_class: hospitality, host policy decides which loads. Recommend the conflict-resolution rules from RFC 0003 (MOD-DISPLAY-CONFLICT-001) extend to overlapping sections too.
Prior art
- OpenAPI extensions (
x-*fields) — established pattern for spec extensions; module sections fill the same role but with stronger schemas. - STR's hospitality data dictionary — vocabulary reference for ADR / RevPAR / occupancy fields.
- HotStats benchmarking schema — vocabulary reference for the F&B and brand-fee fields.
- JSON Schema's
$defsand reusable schemas — mechanism we use to share schema fragments between the module manifest and the conformance fixtures. - The
crd(Crossplane) pattern in Kubernetes — declarative type extension via a manifest, with a registry that resolves references. Same shape as what we're doing, in a different domain.