RFC 0003: Custom asset-class declarations from modules
Summary
AssetClass is hard-coded as a closed enum in types.ts:18. Every new asset class — gas stations, life-sciences, manufactured housing, data centers — requires a spec bump. This RFC proposes letting modules declare new asset-class identifiers, with namespacing rules that prevent collision and a host-side resolution model that keeps determinism.
Motivation
The closed enum is correct for the v1 release: it eliminates the question "what does data_center mean?" by simply not allowing it. But it forces every adopter who wants a new asset class to either:
- Lobby for it to land in the core enum — slow, doesn't scale.
- Reuse an existing class incorrectly — e.g. data centers as
industrial, which then breaks the validation rules (a data center has no warehouse square footage, but an industrial validator may require it). - Fork the spec — defeats the whole interoperability goal.
A module is already the right boundary for "extends the standard." It can declare new sections, calculations, validations. The asset class is the one extension point modules cannot reach.
Proposed change
Asset-class identifier namespacing
A custom asset class identifier MUST be reverse-DNS, lower-snake-case between dots, and contain at least two dots:
org.uwmd.data_center
com.example.specialty.boutique_officeBuilt-in asset classes (the current enum values) are reserved as unnamespaced identifiers. The grammar:
asset_class := builtin | custom
builtin := 'multifamily' | 'office' | 'retail' | 'industrial'
| 'self_storage' | 'hospitality' | 'mixed_use'
| 'senior_housing' | 'student_housing' | 'land'
custom := dns_segment ('.' dns_segment){2,}
dns_segment := [a-z][a-z0-9_]*A parser MUST reject identifiers that are neither a built-in nor a valid namespaced identifier with INVALID-ASSET-CLASS-001.
Module declaration
ModuleManifest.asset_classes becomes the registry surface:
export interface ModuleAssetClassDecl {
/** The namespaced identifier this module introduces. */
id: string;
/** Human-readable display name. */
display_name: string;
/** The closest built-in for fallback rendering by readers that don't load this module. */
fallback?: AssetClass;
/** Required sections specific to this asset class. */
required_sections?: string[];
/** Optional sections specific to this asset class. */
optional_sections?: string[];
}
export interface ModuleManifest {
// ...
/** Custom asset-class declarations introduced by this module. */
declares_asset_classes?: ModuleAssetClassDecl[];
/** Built-in asset classes this module enhances (existing field, semantics unchanged). */
asset_classes?: AssetClass[];
}A .uw.md file whose asset_class is namespaced MUST declare in frontmatter the modules it relies on:
asset_class: org.uwmd.data_center
modules:
- id: org.uwmd.data_center_module
version: ">=0.1.0 <1.0.0"Host resolution
When parsing a file with a custom asset class, the host:
- Looks up the declaring module from the file's
moduleslist. - If the module is loaded and registers
asset_class.id, the host treats the file as that asset class. - If the module is unavailable but a
fallbackis declared, the host MAY render with the fallback's view models, MUST mark the file asdegradedin its parse result, and MUST emitMOD-FALLBACK-001(warning). - If neither the module nor a fallback is available, the host emits
MOD-MISSING-001(error).
This keeps determinism: every host that has the module loaded produces the same result; every host that doesn't either falls back identically or errors identically.
Library
AssetClassbecomes:tsexport type BuiltinAssetClass = /* the current 10 values */; export type CustomAssetClass = string; // validated at parse time export type AssetClass = BuiltinAssetClass | CustomAssetClass;- New
parseAssetClass(raw: string): { kind: 'builtin' | 'custom'; id: string } | ProtocolError. ModuleRegistry.registerAssetClass(decl)— registers a declaration so subsequent file loads can resolve it.
Compatibility analysis
- Existing
.uw.mdfiles — no breakage. Files using built-in asset classes continue to parse identically. - Tier-1 readers that don't load modules — they encounter a custom asset class, follow the fallback path. With a fallback declared, the file still renders (degraded); without one, it errors. Either outcome is deterministic.
- Tier-2 editors — preserve the
asset_classandmoduleskeys on round-trip. - Tier-3 calc hosts — calculations attached to a custom asset class come from the module; if the module is loaded, they work. No core change needed.
- Tier-4 agent hosts — same as calc hosts; agent layer declarations come from the module.
- Modules — manifest schema gains the additive
declares_asset_classesfield. Existing modules without it continue to load.
No deprecation path required — additive.
Conformance impact
New fixtures in conformance/tier-1-reader/fixtures/:
custom-asset-class-with-module/— file declaresorg.uwmd.data_center, module is in the test fixtures dir, expected parse + render uses module's view models.custom-asset-class-fallback/— same file, module not loaded butfallback: industrialdeclared, expectedMOD-FALLBACK-001warning + render via industrial view model.custom-asset-class-missing/— module not loaded, no fallback, expectedMOD-MISSING-001error.invalid-asset-class-syntax/— file withasset_class: DataCenter, expectedINVALID-ASSET-CLASS-001error.
Existing fixtures using built-in asset classes pass without modification.
Reference implementation
- Files affected:
packages/uwmd-core/src/types.ts—AssetClasstype split.packages/uwmd-core/src/parser.ts— asset-class identifier validation.packages/uwmd-core/src/module-registry.ts(new) — register/resolve declarations.packages/uwmd-core/src/protocol.ts—ModuleAssetClassDecl, extendModuleManifest.spec/UW_FORMAT_SPEC_v1.md— namespacing grammar.spec/UW_PROTOCOL_v1.md— Part X resolution rules.spec/schemas/module-manifest.schema.json— additive field.
- API surface:
parseAssetClass,ModuleRegistry, plus the new manifest field. - Test plan: unit tests on
parseAssetClass. Integration test loads a fixture module manifest, registers a custom asset class, parses a file referencing it, asserts the file's view-model resolution uses the module's declarations.
Alternatives considered
Open enum (any string accepted). Rejected — loses determinism. Two hosts seeing
data_centercould mean different things; the namespacing rule fixes ownership at the identifier level.URI-style identifiers (e.g.
https://uwmd.org/asset-classes/data-center). Rejected — overspecified. URIs imply HTTP resolution semantics we don't want; reverse-DNS gets the namespacing benefit without that baggage.Subclass tree (e.g.
industrial.data_center). Considered. Solves the fallback problem implicitly (parent class is the fallback). Rejected because not all custom classes have a clean built-in parent — a life-sciences building isn't really a subclass ofofficeorindustrial. The explicitfallbackfield handles this case better.Modules can't declare new asset classes; spec bumps still required. The status quo. Rejected — doesn't scale, see Motivation.
Unresolved questions
- Identifier squatting. What stops
com.example.multifamilyfrom being declared? Recommend:INVALID-ASSET-CLASS-002if a custom identifier ends in (or matches) a built-in. The reserved names list should be checked against the full identifier, not just trailing segment. - Display-name conflicts. Two unrelated modules could both declare
display_name: "Data Center". The host displays whichever module is loaded; if both are loaded, host policy decides. Recommend host emitsMOD-DISPLAY-CONFLICT-001(info) when this happens. - Conformance corpus — should it ship a fixture custom asset class for testing purposes? Recommend yes, under
conformance/fixtures/modules/so it's reusable across tier-1/2/3 fixtures.
Prior art
- MIME types — closed top-level types (
text,image, …) plus open subtypes via thevnd.*andprs.*prefixes. Same shape: closed at the top, open below. - Reverse-DNS namespacing — used by Java packages, Android intents, OSGi bundles. Mature, well-understood.
- Schema.org extensions — uses URI-style identifiers; their experience documents the cost of HTTP-resolvable identifiers (see Alternatives 2).
- Linux Foundation TODO Group / SPDX — manages a registry of license identifiers that's open to additions but maintains a canonical list. The closest analog to where we'd want a UW Markdown asset-class registry to live.