@uwmd/core 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +79 -0
  2. package/dist/agents/bancroft.d.ts +32 -0
  3. package/dist/agents/bancroft.d.ts.map +1 -0
  4. package/dist/agents/bancroft.js +292 -0
  5. package/dist/agents/bancroft.js.map +1 -0
  6. package/dist/agents/schemas.d.ts +13 -0
  7. package/dist/agents/schemas.d.ts.map +1 -0
  8. package/dist/agents/schemas.js +74 -0
  9. package/dist/agents/schemas.js.map +1 -0
  10. package/dist/bindings.d.ts +85 -0
  11. package/dist/bindings.d.ts.map +1 -0
  12. package/dist/bindings.js +293 -0
  13. package/dist/bindings.js.map +1 -0
  14. package/dist/browser.d.ts +66 -0
  15. package/dist/browser.d.ts.map +1 -0
  16. package/dist/browser.js +53 -0
  17. package/dist/browser.js.map +1 -0
  18. package/dist/calc/builtins.d.ts +4 -0
  19. package/dist/calc/builtins.d.ts.map +1 -0
  20. package/dist/calc/builtins.js +329 -0
  21. package/dist/calc/builtins.js.map +1 -0
  22. package/dist/calc/dependencies.d.ts +34 -0
  23. package/dist/calc/dependencies.d.ts.map +1 -0
  24. package/dist/calc/dependencies.js +101 -0
  25. package/dist/calc/dependencies.js.map +1 -0
  26. package/dist/calc/errors.d.ts +8 -0
  27. package/dist/calc/errors.d.ts.map +1 -0
  28. package/dist/calc/errors.js +18 -0
  29. package/dist/calc/errors.js.map +1 -0
  30. package/dist/calc/evaluator.d.ts +5 -0
  31. package/dist/calc/evaluator.d.ts.map +1 -0
  32. package/dist/calc/evaluator.js +184 -0
  33. package/dist/calc/evaluator.js.map +1 -0
  34. package/dist/calc/index.d.ts +14 -0
  35. package/dist/calc/index.d.ts.map +1 -0
  36. package/dist/calc/index.js +56 -0
  37. package/dist/calc/index.js.map +1 -0
  38. package/dist/calc/parser.d.ts +32 -0
  39. package/dist/calc/parser.d.ts.map +1 -0
  40. package/dist/calc/parser.js +361 -0
  41. package/dist/calc/parser.js.map +1 -0
  42. package/dist/cascade.d.ts +85 -0
  43. package/dist/cascade.d.ts.map +1 -0
  44. package/dist/cascade.js +172 -0
  45. package/dist/cascade.js.map +1 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.d.ts.map +1 -0
  48. package/dist/cli.js +843 -0
  49. package/dist/cli.js.map +1 -0
  50. package/dist/codec.d.ts +25 -0
  51. package/dist/codec.d.ts.map +1 -0
  52. package/dist/codec.js +62 -0
  53. package/dist/codec.js.map +1 -0
  54. package/dist/codecs.d.ts +6 -0
  55. package/dist/codecs.d.ts.map +1 -0
  56. package/dist/codecs.js +12 -0
  57. package/dist/codecs.js.map +1 -0
  58. package/dist/compactor.d.ts +11 -0
  59. package/dist/compactor.d.ts.map +1 -0
  60. package/dist/compactor.js +84 -0
  61. package/dist/compactor.js.map +1 -0
  62. package/dist/context-profiles.d.ts +30 -0
  63. package/dist/context-profiles.d.ts.map +1 -0
  64. package/dist/context-profiles.js +225 -0
  65. package/dist/context-profiles.js.map +1 -0
  66. package/dist/context.d.ts +55 -0
  67. package/dist/context.d.ts.map +1 -0
  68. package/dist/context.js +288 -0
  69. package/dist/context.js.map +1 -0
  70. package/dist/dcf.d.ts +9 -0
  71. package/dist/dcf.d.ts.map +1 -0
  72. package/dist/dcf.js +75 -0
  73. package/dist/dcf.js.map +1 -0
  74. package/dist/debt.d.ts +11 -0
  75. package/dist/debt.d.ts.map +1 -0
  76. package/dist/debt.js +85 -0
  77. package/dist/debt.js.map +1 -0
  78. package/dist/defaults.d.ts +104 -0
  79. package/dist/defaults.d.ts.map +1 -0
  80. package/dist/defaults.js +622 -0
  81. package/dist/defaults.js.map +1 -0
  82. package/dist/derived.d.ts +17 -0
  83. package/dist/derived.d.ts.map +1 -0
  84. package/dist/derived.js +29 -0
  85. package/dist/derived.js.map +1 -0
  86. package/dist/editor.d.ts +76 -0
  87. package/dist/editor.d.ts.map +1 -0
  88. package/dist/editor.js +502 -0
  89. package/dist/editor.js.map +1 -0
  90. package/dist/envelope.d.ts +51 -0
  91. package/dist/envelope.d.ts.map +1 -0
  92. package/dist/envelope.js +213 -0
  93. package/dist/envelope.js.map +1 -0
  94. package/dist/format.d.ts +60 -0
  95. package/dist/format.d.ts.map +1 -0
  96. package/dist/format.js +115 -0
  97. package/dist/format.js.map +1 -0
  98. package/dist/gaps.d.ts +62 -0
  99. package/dist/gaps.d.ts.map +1 -0
  100. package/dist/gaps.js +272 -0
  101. package/dist/gaps.js.map +1 -0
  102. package/dist/index.d.ts +75 -0
  103. package/dist/index.d.ts.map +1 -0
  104. package/dist/index.js +47 -0
  105. package/dist/index.js.map +1 -0
  106. package/dist/init.d.ts +15 -0
  107. package/dist/init.d.ts.map +1 -0
  108. package/dist/init.js +169 -0
  109. package/dist/init.js.map +1 -0
  110. package/dist/integrity-canonical.d.ts +17 -0
  111. package/dist/integrity-canonical.d.ts.map +1 -0
  112. package/dist/integrity-canonical.js +124 -0
  113. package/dist/integrity-canonical.js.map +1 -0
  114. package/dist/integrity.d.ts +54 -0
  115. package/dist/integrity.d.ts.map +1 -0
  116. package/dist/integrity.js +244 -0
  117. package/dist/integrity.js.map +1 -0
  118. package/dist/modules.d.ts +24 -0
  119. package/dist/modules.d.ts.map +1 -0
  120. package/dist/modules.js +322 -0
  121. package/dist/modules.js.map +1 -0
  122. package/dist/negotiation.d.ts +19 -0
  123. package/dist/negotiation.d.ts.map +1 -0
  124. package/dist/negotiation.js +70 -0
  125. package/dist/negotiation.js.map +1 -0
  126. package/dist/opstatement.d.ts +9 -0
  127. package/dist/opstatement.d.ts.map +1 -0
  128. package/dist/opstatement.js +80 -0
  129. package/dist/opstatement.js.map +1 -0
  130. package/dist/packs/excel-emit.d.ts +21 -0
  131. package/dist/packs/excel-emit.d.ts.map +1 -0
  132. package/dist/packs/excel-emit.js +135 -0
  133. package/dist/packs/excel-emit.js.map +1 -0
  134. package/dist/packs/index.d.ts +13 -0
  135. package/dist/packs/index.d.ts.map +1 -0
  136. package/dist/packs/index.js +37 -0
  137. package/dist/packs/index.js.map +1 -0
  138. package/dist/packs/industrial.d.ts +3 -0
  139. package/dist/packs/industrial.d.ts.map +1 -0
  140. package/dist/packs/industrial.js +115 -0
  141. package/dist/packs/industrial.js.map +1 -0
  142. package/dist/packs/multifamily.d.ts +3 -0
  143. package/dist/packs/multifamily.d.ts.map +1 -0
  144. package/dist/packs/multifamily.js +82 -0
  145. package/dist/packs/multifamily.js.map +1 -0
  146. package/dist/packs/office.d.ts +3 -0
  147. package/dist/packs/office.d.ts.map +1 -0
  148. package/dist/packs/office.js +108 -0
  149. package/dist/packs/office.js.map +1 -0
  150. package/dist/packs/retail.d.ts +3 -0
  151. package/dist/packs/retail.d.ts.map +1 -0
  152. package/dist/packs/retail.js +115 -0
  153. package/dist/packs/retail.js.map +1 -0
  154. package/dist/packs/self-storage.d.ts +3 -0
  155. package/dist/packs/self-storage.d.ts.map +1 -0
  156. package/dist/packs/self-storage.js +105 -0
  157. package/dist/packs/self-storage.js.map +1 -0
  158. package/dist/parser.d.ts +15 -0
  159. package/dist/parser.d.ts.map +1 -0
  160. package/dist/parser.js +430 -0
  161. package/dist/parser.js.map +1 -0
  162. package/dist/protocol.d.ts +389 -0
  163. package/dist/protocol.d.ts.map +1 -0
  164. package/dist/protocol.js +714 -0
  165. package/dist/protocol.js.map +1 -0
  166. package/dist/refinement.d.ts +63 -0
  167. package/dist/refinement.d.ts.map +1 -0
  168. package/dist/refinement.js +242 -0
  169. package/dist/refinement.js.map +1 -0
  170. package/dist/renderer.d.ts +22 -0
  171. package/dist/renderer.d.ts.map +1 -0
  172. package/dist/renderer.js +478 -0
  173. package/dist/renderer.js.map +1 -0
  174. package/dist/rentroll.d.ts +21 -0
  175. package/dist/rentroll.d.ts.map +1 -0
  176. package/dist/rentroll.js +256 -0
  177. package/dist/rentroll.js.map +1 -0
  178. package/dist/report.d.ts +29 -0
  179. package/dist/report.d.ts.map +1 -0
  180. package/dist/report.js +924 -0
  181. package/dist/report.js.map +1 -0
  182. package/dist/runner.d.ts +43 -0
  183. package/dist/runner.d.ts.map +1 -0
  184. package/dist/runner.js +288 -0
  185. package/dist/runner.js.map +1 -0
  186. package/dist/sourcesuses.d.ts +14 -0
  187. package/dist/sourcesuses.d.ts.map +1 -0
  188. package/dist/sourcesuses.js +63 -0
  189. package/dist/sourcesuses.js.map +1 -0
  190. package/dist/types.d.ts +248 -0
  191. package/dist/types.d.ts.map +1 -0
  192. package/dist/types.js +15 -0
  193. package/dist/types.js.map +1 -0
  194. package/dist/uwcsv.d.ts +23 -0
  195. package/dist/uwcsv.d.ts.map +1 -0
  196. package/dist/uwcsv.js +877 -0
  197. package/dist/uwcsv.js.map +1 -0
  198. package/dist/uwjson.d.ts +19 -0
  199. package/dist/uwjson.d.ts.map +1 -0
  200. package/dist/uwjson.js +61 -0
  201. package/dist/uwjson.js.map +1 -0
  202. package/dist/uwxml.d.ts +18 -0
  203. package/dist/uwxml.d.ts.map +1 -0
  204. package/dist/uwxml.js +297 -0
  205. package/dist/uwxml.js.map +1 -0
  206. package/dist/validator.d.ts +35 -0
  207. package/dist/validator.d.ts.map +1 -0
  208. package/dist/validator.js +553 -0
  209. package/dist/validator.js.map +1 -0
  210. package/dist/valuation.d.ts +9 -0
  211. package/dist/valuation.d.ts.map +1 -0
  212. package/dist/valuation.js +34 -0
  213. package/dist/valuation.js.map +1 -0
  214. package/package.json +58 -0
@@ -0,0 +1,714 @@
1
+ // UW Protocol — TypeScript contract surface for conforming implementers.
2
+ //
3
+ // This module is the executable counterpart to spec/UW_PROTOCOL_v1.md.
4
+ // Where the prose document defines normative behavior, this module names the
5
+ // types implementers must satisfy and exports the BUILTIN_* tables that any
6
+ // Tier-1+ Reader, Tier-2+ Editor, Tier-3+ Calc Host, or Tier-4 Agent Host
7
+ // references.
8
+ //
9
+ // Types defined in `./types.ts` are re-used here, never duplicated. The
10
+ // protocol surface is a layer over the format types — same way OpenAPI
11
+ // operations are a layer over JSON Schema.
12
+ // ─── Versioning ───────────────────────────────────────────────────────────────
13
+ /** Semver of this protocol. Bumped independently of @uwmd/core's npm version. */
14
+ export const PROTOCOL_VERSION = '1.2.0';
15
+ /** Format spec version this protocol pairs with. */
16
+ export const FORMAT_VERSION = '1.1';
17
+ /** Default number-formatting table referenced by Part III §3.1. */
18
+ export const DEFAULT_NUMBER_FORMAT = {
19
+ locale: 'en-US',
20
+ currency: { decimals: 0, symbol: '$' },
21
+ percent: { decimals: 2, multiplier: 100, suffix: '%' },
22
+ ratio: { decimals: 3, suffix: 'x' },
23
+ count: { thousands_separator: true },
24
+ null_display: 'n/a',
25
+ };
26
+ export const DEFAULT_DATE_FORMAT = {
27
+ locale: 'en-US',
28
+ default_style: 'iso',
29
+ };
30
+ /** Ordered cascade as a runtime value (frozen). Index === precedence. */
31
+ export const CASCADE_ORDER = Object.freeze([
32
+ 'user_override',
33
+ 'user_input',
34
+ 'investor_profile',
35
+ 'market_data',
36
+ 'asset_class_default',
37
+ 'global_default',
38
+ 'system_default',
39
+ ]);
40
+ /**
41
+ * The full set of canonical short-form source tags producers stamp into
42
+ * `_meta.source`. The first seven values match `CascadeStep` 1:1; the
43
+ * remaining four are non-cascade tags retained from v1.0 for back-compat
44
+ * (`manual`, `ai_extracted`, `agent_computed`, `scenario_default`).
45
+ *
46
+ * `scenario_default` is retained but its meaning is sharpened to mean
47
+ * "value derived from a named scenario in the file or institution
48
+ * config." Producers needing a generic fallback SHOULD use
49
+ * `system_default` instead.
50
+ *
51
+ * Long-form patterns (e.g. `agent/L6-01`, `document/rent_roll`,
52
+ * `import:filename.pdf`) remain valid; this constant enumerates the
53
+ * canonical short forms only.
54
+ */
55
+ export const SOURCE_TAGS = Object.freeze([
56
+ 'user_input',
57
+ 'user_override',
58
+ 'manual',
59
+ 'investor_profile',
60
+ 'market_data',
61
+ 'ai_extracted',
62
+ 'agent_computed',
63
+ 'asset_class_default',
64
+ 'scenario_default',
65
+ 'global_default',
66
+ 'system_default',
67
+ ]);
68
+ /**
69
+ * Curated default policies covering high-impact (section, stage) pairs in the
70
+ * multifamily workflow. Adopters MAY extend this set; lookups consult adopter
71
+ * policies first, then these defaults.
72
+ *
73
+ * Policies stack: more-specific (with field_path) wins over less-specific;
74
+ * within the same specificity, more-specific stage wins over wildcard.
75
+ */
76
+ export const BUILTIN_INCOMPLETE_DATA_POLICIES = Object.freeze([
77
+ // ─── rent_roll ─────────────────────────────────────────────────────────────
78
+ {
79
+ section: 'rent_roll',
80
+ stage: 'scope',
81
+ action: { kind: 'substitute', fallback_source: 'asset_class_default' },
82
+ rationale: 'Scope-stage triage: assume default vacancy/occupancy.',
83
+ },
84
+ {
85
+ section: 'rent_roll',
86
+ stage: 'screening',
87
+ action: { kind: 'degrade' },
88
+ rationale: 'Screening tolerates estimates; surface as conditional.',
89
+ },
90
+ {
91
+ section: 'rent_roll',
92
+ stage: 'full_underwrite',
93
+ action: { kind: 'halt' },
94
+ rationale: 'Full underwrite requires the actual rent roll.',
95
+ },
96
+ // ─── noi_model ─────────────────────────────────────────────────────────────
97
+ {
98
+ section: 'noi_model',
99
+ field_path: 'expense_ratio',
100
+ stage: 'scope',
101
+ action: { kind: 'substitute', fallback_source: 'asset_class_default' },
102
+ },
103
+ {
104
+ section: 'noi_model',
105
+ stage: 'scope',
106
+ action: { kind: 'substitute', fallback_source: 'asset_class_default' },
107
+ },
108
+ {
109
+ section: 'noi_model',
110
+ stage: 'screening',
111
+ action: { kind: 'degrade' },
112
+ },
113
+ {
114
+ section: 'noi_model',
115
+ stage: 'full_underwrite',
116
+ action: { kind: 'halt' },
117
+ rationale: 'Full underwrite requires a T-12 / proforma derived NOI.',
118
+ },
119
+ // ─── debt_structure ────────────────────────────────────────────────────────
120
+ {
121
+ section: 'debt_structure',
122
+ stage: 'scope',
123
+ action: { kind: 'substitute', fallback_source: 'asset_class_default' },
124
+ },
125
+ {
126
+ section: 'debt_structure',
127
+ stage: 'screening',
128
+ action: { kind: 'substitute', fallback_source: 'investor_profile' },
129
+ },
130
+ {
131
+ section: 'debt_structure',
132
+ stage: 'term_sheet',
133
+ action: { kind: 'halt' },
134
+ rationale: 'Term sheet stage requires concrete debt terms.',
135
+ },
136
+ // ─── valuation ─────────────────────────────────────────────────────────────
137
+ {
138
+ section: 'valuation',
139
+ stage: 'scope',
140
+ action: { kind: 'substitute', fallback_source: 'asset_class_default' },
141
+ },
142
+ {
143
+ section: 'valuation',
144
+ stage: 'full_underwrite',
145
+ action: { kind: 'halt' },
146
+ },
147
+ // ─── borrower_sponsor ──────────────────────────────────────────────────────
148
+ {
149
+ section: 'borrower_sponsor',
150
+ stage: 'screening',
151
+ action: { kind: 'defer' },
152
+ rationale: 'Sponsor diligence often arrives later; record but allow advance.',
153
+ },
154
+ {
155
+ section: 'borrower_sponsor',
156
+ stage: 'credit_approval',
157
+ action: { kind: 'halt' },
158
+ },
159
+ // ─── compliance ────────────────────────────────────────────────────────────
160
+ {
161
+ section: 'compliance',
162
+ stage: 'closing',
163
+ action: { kind: 'halt' },
164
+ rationale: 'Compliance gaps cannot be carried into closing.',
165
+ },
166
+ ]);
167
+ /**
168
+ * Look up the most-specific applicable policy for `(section, field_path, stage)`.
169
+ *
170
+ * Specificity order (highest to lowest):
171
+ * 1. matching section + matching field_path + matching stage
172
+ * 2. matching section + matching field_path (any stage)
173
+ * 3. matching section + matching stage (any field)
174
+ * 4. matching section (any field, any stage)
175
+ *
176
+ * Returns null when no policy matches.
177
+ */
178
+ export function lookupIncompleteDataPolicy(section, field_path, stage, policies = BUILTIN_INCOMPLETE_DATA_POLICIES) {
179
+ let best = null;
180
+ let bestScore = -1;
181
+ for (const p of policies) {
182
+ if (p.section !== section)
183
+ continue;
184
+ const fieldMatch = p.field_path === undefined || p.field_path === field_path;
185
+ if (!fieldMatch)
186
+ continue;
187
+ const stageMatch = p.stage === undefined || p.stage === stage;
188
+ if (!stageMatch)
189
+ continue;
190
+ let score = 0;
191
+ if (p.field_path !== undefined)
192
+ score += 4;
193
+ if (p.stage !== undefined)
194
+ score += 2;
195
+ // Tie-break: prefer policies for the exact stage over wildcard, even if
196
+ // both have field_path defined.
197
+ if (p.stage === stage)
198
+ score += 1;
199
+ if (score > bestScore) {
200
+ bestScore = score;
201
+ best = p;
202
+ }
203
+ }
204
+ return best;
205
+ }
206
+ // ─── Built-in tables ──────────────────────────────────────────────────────────
207
+ /**
208
+ * View models for the 21 standard sections defined in UW_FORMAT_SPEC_v1.md §4.
209
+ * Implementers SHOULD use these as the default rendering layout. Modules MAY
210
+ * override per-section by declaring their own `view_models` entry.
211
+ */
212
+ export const BUILTIN_VIEW_MODELS = Object.freeze({
213
+ property: {
214
+ section_id: 'property',
215
+ display_name: 'Property',
216
+ display_order: 0,
217
+ description: 'Physical asset description: units, vintage, class, amenities.',
218
+ primary_fields: [
219
+ { path: 'total_units', label: 'Units', kind: 'count', primary: true, unit: 'units' },
220
+ { path: 'year_built', label: 'Built', kind: 'count' },
221
+ { path: 'building_class', label: 'Class', kind: 'enum', enum: ['A', 'B', 'C', 'D'] },
222
+ { path: 'asset_subtype', label: 'Subtype', kind: 'string' },
223
+ ],
224
+ detail_fields: [
225
+ { path: 'year_renovated', label: 'Renovated', kind: 'count' },
226
+ { path: 'total_nra_sqft', label: 'NRA', kind: 'count', unit: 'sqft' },
227
+ { path: 'land_area_acres', label: 'Land', kind: 'ratio', decimals: 2, unit: 'acres' },
228
+ { path: 'parking_spaces', label: 'Parking', kind: 'count', unit: 'spaces' },
229
+ { path: 'zoning', label: 'Zoning', kind: 'string' },
230
+ { path: 'condition', label: 'Condition', kind: 'string' },
231
+ { path: 'deferred_maintenance_est', label: 'Deferred Maint', kind: 'currency' },
232
+ { path: 'amenities', label: 'Amenities', kind: 'list' },
233
+ ],
234
+ },
235
+ rent_roll: {
236
+ section_id: 'rent_roll',
237
+ display_name: 'Rent Roll',
238
+ display_order: 1,
239
+ description: 'Unit-level lease detail and current rent state.',
240
+ primary_fields: [
241
+ { path: 'total_units', label: 'Total Units', kind: 'count', primary: true },
242
+ { path: 'occupied_units', label: 'Occupied', kind: 'count', primary: true },
243
+ { path: 'physical_occupancy', label: 'Occupancy', kind: 'percent', primary: true },
244
+ { path: 'avg_rent', label: 'Avg Rent', kind: 'currency' },
245
+ ],
246
+ },
247
+ noi_model: {
248
+ section_id: 'noi_model',
249
+ display_name: 'NOI Model',
250
+ display_order: 2,
251
+ description: 'Underwritten T-12 normalized operating statement.',
252
+ primary_fields: [
253
+ { path: 'net_operating_income', label: 'NOI', kind: 'currency', primary: true },
254
+ { path: 'effective_gross_income', label: 'EGI', kind: 'currency' },
255
+ { path: 'total_operating_expenses', label: 'OpEx', kind: 'currency' },
256
+ { path: 'opex_ratio', label: 'OpEx Ratio', kind: 'percent' },
257
+ ],
258
+ detail_fields: [
259
+ { path: 'gross_potential_rent', label: 'GPR', kind: 'currency' },
260
+ { path: 'vacancy_loss', label: 'Vacancy Loss', kind: 'currency' },
261
+ { path: 'vacancy_rate', label: 'Vacancy Rate', kind: 'percent' },
262
+ { path: 'other_income', label: 'Other Income', kind: 'currency' },
263
+ ],
264
+ },
265
+ debt_structure: {
266
+ section_id: 'debt_structure',
267
+ display_name: 'Debt',
268
+ display_order: 3,
269
+ description: 'Loan terms and computed debt metrics.',
270
+ primary_fields: [
271
+ { path: 'loan_amount', label: 'Loan', kind: 'currency', primary: true },
272
+ { path: 'interest_rate', label: 'Rate', kind: 'percent', primary: true },
273
+ { path: 'dscr', label: 'DSCR', kind: 'ratio', primary: true },
274
+ { path: 'ltv', label: 'LTV', kind: 'percent', primary: true },
275
+ ],
276
+ detail_fields: [
277
+ { path: 'loan_term_years', label: 'Term', kind: 'count', unit: 'yrs' },
278
+ { path: 'amortization_years', label: 'Amortization', kind: 'count', unit: 'yrs' },
279
+ { path: 'io_period_months', label: 'IO Period', kind: 'count', unit: 'mo' },
280
+ { path: 'debt_yield', label: 'Debt Yield', kind: 'percent' },
281
+ { path: 'annual_debt_service', label: 'Annual DS', kind: 'currency' },
282
+ { path: 'recourse', label: 'Recourse', kind: 'string' },
283
+ ],
284
+ },
285
+ valuation: {
286
+ section_id: 'valuation',
287
+ display_name: 'Valuation',
288
+ display_order: 4,
289
+ description: 'Purchase price, underwritten value, comparable analysis.',
290
+ primary_fields: [
291
+ { path: 'purchase_price', label: 'Purchase Price', kind: 'currency', primary: true },
292
+ { path: 'underwritten_value', label: 'UW Value', kind: 'currency' },
293
+ { path: 'going_in_cap_rate', label: 'Going-in Cap', kind: 'percent', primary: true },
294
+ { path: 'price_per_unit', label: 'Price/Unit', kind: 'currency' },
295
+ ],
296
+ },
297
+ dcf: {
298
+ section_id: 'dcf',
299
+ display_name: 'DCF',
300
+ display_order: 5,
301
+ description: 'Discounted cash flow, hold-period assumptions, IRR.',
302
+ primary_fields: [
303
+ { path: 'levered_irr', label: 'Levered IRR', kind: 'percent', primary: true },
304
+ { path: 'levered_equity_multiple', label: 'Equity Multiple', kind: 'ratio', primary: true },
305
+ { path: 'assumptions.hold_period_years', label: 'Hold', kind: 'count', unit: 'yrs' },
306
+ { path: 'assumptions.exit_cap_rate', label: 'Exit Cap', kind: 'percent' },
307
+ ],
308
+ },
309
+ sources_uses: {
310
+ section_id: 'sources_uses',
311
+ display_name: 'Sources & Uses',
312
+ display_order: 6,
313
+ description: 'Capital stack reconciliation.',
314
+ primary_fields: [
315
+ { path: 'total_sources', label: 'Total Sources', kind: 'currency', primary: true },
316
+ { path: 'total_uses', label: 'Total Uses', kind: 'currency', primary: true },
317
+ ],
318
+ },
319
+ market_analysis: {
320
+ section_id: 'market_analysis',
321
+ display_name: 'Market',
322
+ display_order: 7,
323
+ description: 'MSA / submarket fundamentals and comp set.',
324
+ primary_fields: [
325
+ { path: 'msa', label: 'MSA', kind: 'string', primary: true },
326
+ { path: 'market_vacancy_rate', label: 'Market Vacancy', kind: 'percent' },
327
+ { path: 'average_asking_rent', label: 'Asking Rent', kind: 'currency' },
328
+ { path: 'recent_rent_growth', label: 'Rent Growth', kind: 'percent' },
329
+ ],
330
+ },
331
+ borrower_sponsor: {
332
+ section_id: 'borrower_sponsor',
333
+ display_name: 'Sponsor',
334
+ display_order: 8,
335
+ description: 'Borrower / sponsor financial strength.',
336
+ primary_fields: [
337
+ { path: 'name', label: 'Sponsor', kind: 'string', primary: true },
338
+ { path: 'net_worth', label: 'Net Worth', kind: 'currency' },
339
+ { path: 'liquidity', label: 'Liquidity', kind: 'currency' },
340
+ { path: 'years_experience', label: 'Experience', kind: 'count', unit: 'yrs' },
341
+ ],
342
+ },
343
+ deal_context: {
344
+ section_id: 'deal_context',
345
+ display_name: 'Deal Context',
346
+ display_order: 9,
347
+ description: 'Investment thesis, value creation, hold strategy narrative.',
348
+ primary_fields: [
349
+ { path: 'investment_thesis', label: 'Thesis', kind: 'string', primary: true },
350
+ { path: 'value_creation_strategy', label: 'Value Creation', kind: 'string' },
351
+ { path: 'hold_strategy', label: 'Hold Strategy', kind: 'string' },
352
+ ],
353
+ },
354
+ stress_tests: {
355
+ section_id: 'stress_tests',
356
+ display_name: 'Stress Tests',
357
+ display_order: 10,
358
+ description: 'Sensitivity scenarios — vacancy, rate, exit cap shocks.',
359
+ multi_variant: true,
360
+ primary_fields: [
361
+ { path: 'name', label: 'Scenario', kind: 'string', primary: true },
362
+ { path: 'dscr', label: 'DSCR', kind: 'ratio' },
363
+ { path: 'irr', label: 'IRR', kind: 'percent' },
364
+ ],
365
+ },
366
+ risk_assessment: {
367
+ section_id: 'risk_assessment',
368
+ display_name: 'Risk Assessment',
369
+ display_order: 11,
370
+ description: 'Aggregate risk rating and key risk factors.',
371
+ primary_fields: [
372
+ { path: 'overall_rating', label: 'Rating', kind: 'string', primary: true },
373
+ { path: 'risk_score', label: 'Score', kind: 'count' },
374
+ { path: 'key_risks', label: 'Key Risks', kind: 'list' },
375
+ ],
376
+ },
377
+ // Remaining standard sections registered with minimal hints; implementers
378
+ // may pull richer view models from their own modules.
379
+ loan_terms_summary: {
380
+ section_id: 'loan_terms_summary', display_name: 'Loan Terms Summary', display_order: 12,
381
+ description: 'Term sheet summary issued to borrower.',
382
+ primary_fields: [],
383
+ },
384
+ closing_conditions: {
385
+ section_id: 'closing_conditions', display_name: 'Closing Conditions', display_order: 13,
386
+ description: 'CPs, post-close covenants, reserve requirements.',
387
+ primary_fields: [],
388
+ },
389
+ third_party_reports: {
390
+ section_id: 'third_party_reports', display_name: 'Third-Party Reports', display_order: 14,
391
+ description: 'Appraisal, PCA, ESA, zoning, survey status.',
392
+ primary_fields: [],
393
+ },
394
+ insurance: {
395
+ section_id: 'insurance', display_name: 'Insurance', display_order: 15,
396
+ description: 'Required coverages and current binder status.',
397
+ primary_fields: [],
398
+ },
399
+ environmental: {
400
+ section_id: 'environmental', display_name: 'Environmental', display_order: 16,
401
+ description: 'Phase I / II findings and recognized environmental conditions.',
402
+ primary_fields: [],
403
+ },
404
+ legal: {
405
+ section_id: 'legal', display_name: 'Legal', display_order: 17,
406
+ description: 'Title, survey, entity structure, ongoing litigation.',
407
+ primary_fields: [],
408
+ },
409
+ compliance: {
410
+ section_id: 'compliance', display_name: 'Compliance', display_order: 18,
411
+ description: 'BSA/AML, OFAC, fair-lending, regulatory checks.',
412
+ primary_fields: [],
413
+ },
414
+ monitoring: {
415
+ section_id: 'monitoring', display_name: 'Monitoring', display_order: 19,
416
+ description: 'Post-close performance tracking and covenant compliance.',
417
+ primary_fields: [],
418
+ },
419
+ recommendation: {
420
+ section_id: 'recommendation', display_name: 'Recommendation', display_order: 20,
421
+ description: 'Final credit recommendation and rationale.',
422
+ primary_fields: [
423
+ { path: 'decision', label: 'Decision', kind: 'string', primary: true },
424
+ { path: 'rationale', label: 'Rationale', kind: 'string' },
425
+ ],
426
+ },
427
+ gaps: {
428
+ section_id: 'gaps', display_name: 'Gaps', display_order: 21,
429
+ description: 'Open data gaps blocking stage advancement or carrying provisional defaults. Maintained by the editor when --maintain-gaps is on; otherwise hand-curated.',
430
+ primary_fields: [
431
+ { path: 'summary.total_open', label: 'Open', kind: 'count', primary: true },
432
+ { path: 'summary.blocking_current_stage', label: 'Blocking Current', kind: 'count' },
433
+ { path: 'summary.blocking_next_stage', label: 'Blocking Next', kind: 'count' },
434
+ ],
435
+ },
436
+ });
437
+ /**
438
+ * Default edit policies derived from canonical `_meta.source` patterns.
439
+ * Implementers consult this table to decide whether a write is a replace
440
+ * or a supersede, and whether the actor is permitted to write at all.
441
+ */
442
+ export const BUILTIN_EDIT_POLICIES = Object.freeze([
443
+ { source_pattern: 'agent/*', authority: 'either', supersede_on_edit: true },
444
+ { source_pattern: 'manual', authority: 'either', supersede_on_edit: false },
445
+ { source_pattern: 'document/*', authority: 'either', supersede_on_edit: true },
446
+ { source_pattern: 'system/*', authority: 'system_only', supersede_on_edit: false },
447
+ { source_pattern: 'institution/*', authority: 'system_only', supersede_on_edit: false },
448
+ ]);
449
+ /**
450
+ * Remediation copy for the cross-cutting consistency checks CC-01..CC-10
451
+ * (see UW_FORMAT_SPEC_v1.md §5.3). Renderers SHOULD surface these strings
452
+ * verbatim in any validation issue list — the goal is uniform UX across
453
+ * every conforming implementation.
454
+ */
455
+ export const BUILTIN_REMEDIATIONS = Object.freeze([
456
+ {
457
+ code: 'CC-01', severity: 'error',
458
+ title: 'NOI mismatch',
459
+ description: 'noi_model.net_operating_income disagrees with quick_metrics.noi_underwritten.',
460
+ remediation: 'Recompute NOI from the noi_model section and update frontmatter.quick_metrics.noi_underwritten.',
461
+ spec_ref: '§5.3 CC-01',
462
+ },
463
+ {
464
+ code: 'CC-02', severity: 'error',
465
+ title: 'DSCR mismatch',
466
+ description: 'debt_structure.dscr disagrees with quick_metrics.dscr.',
467
+ remediation: 'Recompute DSCR = NOI / annual_debt_service and update both surfaces.',
468
+ spec_ref: '§5.3 CC-02',
469
+ },
470
+ {
471
+ code: 'CC-03', severity: 'error',
472
+ title: 'LTV mismatch',
473
+ description: 'debt_structure.ltv disagrees with quick_metrics.ltv.',
474
+ remediation: 'Recompute LTV = loan_amount / purchase_price and update both surfaces.',
475
+ spec_ref: '§5.3 CC-03',
476
+ },
477
+ {
478
+ code: 'CC-04', severity: 'error',
479
+ title: 'Sources & Uses imbalance',
480
+ description: 'sources_uses.total_sources does not equal total_uses.',
481
+ remediation: 'Reconcile capital stack — equity + debt proceeds + other sources MUST equal price + closing + reserves.',
482
+ spec_ref: '§5.3 CC-04',
483
+ },
484
+ {
485
+ code: 'CC-05', severity: 'error',
486
+ title: 'Cap rate mismatch',
487
+ description: 'valuation.going_in_cap_rate disagrees with quick_metrics.cap_rate.',
488
+ remediation: 'Recompute cap rate = NOI / purchase_price and align both surfaces.',
489
+ spec_ref: '§5.3 CC-05',
490
+ },
491
+ {
492
+ code: 'CC-06', severity: 'warning',
493
+ title: 'Debt yield mismatch',
494
+ description: 'debt_structure.debt_yield disagrees with quick_metrics.debt_yield.',
495
+ remediation: 'Recompute debt yield = NOI / loan_amount and align both surfaces.',
496
+ spec_ref: '§5.3 CC-06',
497
+ },
498
+ {
499
+ code: 'CC-07', severity: 'warning',
500
+ title: 'IRR / equity multiple mismatch',
501
+ description: 'dcf.levered_irr disagrees with quick_metrics.irr_projected.',
502
+ remediation: 'Re-run the DCF and update quick_metrics.irr_projected from dcf.levered_irr.',
503
+ spec_ref: '§5.3 CC-07',
504
+ },
505
+ {
506
+ code: 'CC-08', severity: 'error',
507
+ title: 'Equity required mismatch',
508
+ description: 'sources_uses-derived equity disagrees with quick_metrics.equity_required.',
509
+ remediation: 'Set quick_metrics.equity_required = sources_uses.sources.equity.',
510
+ spec_ref: '§5.3 CC-08',
511
+ },
512
+ {
513
+ code: 'CC-09', severity: 'warning',
514
+ title: 'Stage readiness gap',
515
+ description: 'A required section for the declared deal_stage is missing or low-confidence.',
516
+ remediation: 'Either complete the required sections for this stage or downgrade frontmatter.deal_stage.',
517
+ spec_ref: '§5.3 CC-09',
518
+ },
519
+ {
520
+ code: 'CC-10', severity: 'info',
521
+ title: 'Stale provenance',
522
+ description: 'A section\'s _meta.timestamp is older than the file last_modified by more than 30 days.',
523
+ remediation: 'Refresh the section, supersede with a new agent/manual write, or mark deliberate stale.',
524
+ spec_ref: '§5.3 CC-10',
525
+ },
526
+ {
527
+ code: 'DQ-01', severity: 'warning',
528
+ title: 'Provisional block without gap entry',
529
+ description: 'A block is marked _meta.provisional=true but no entry in the `gaps` section references it.',
530
+ remediation: 'Add a `gaps` item naming this section/path (or run editor with --maintain-gaps), or remove the provisional flag.',
531
+ spec_ref: 'UW_FORMAT_SPEC_v1.md §3.4 / §4.22',
532
+ },
533
+ {
534
+ code: 'DQ-02', severity: 'error',
535
+ title: 'Provisional value consumed at incompatible stage',
536
+ description: 'A provisional / placeholder value is being consumed at a stage whose policy requires real data.',
537
+ remediation: 'Replace the provisional value with observed data, downgrade deal_stage, or update INCOMPLETE_DATA_POLICIES if appropriate.',
538
+ spec_ref: 'UW_PROTOCOL_v1.md §V.7-§V.8',
539
+ },
540
+ {
541
+ code: 'DQ-03', severity: 'warning',
542
+ title: 'Partial block without field-level enumeration',
543
+ description: 'A block is marked _meta.partial=true but has no _meta.field_overrides[] enumeration.',
544
+ remediation: 'List the affected paths in field_overrides with reason ("missing"|"illegible"|"estimated").',
545
+ spec_ref: 'UW_FORMAT_SPEC_v1.md §3.4',
546
+ },
547
+ {
548
+ code: 'DQ-04', severity: 'error',
549
+ title: 'Scope-stage readiness gap',
550
+ description: 'A field required for scope-stage readiness is missing.',
551
+ remediation: 'Provide the missing scope-stage field (property.address, property.asset_class, and at least one of property.units or property.asking_price) or downgrade deal_stage.',
552
+ spec_ref: 'UW_FORMAT_SPEC_v1.md §2.2',
553
+ },
554
+ {
555
+ code: 'DQ-05', severity: 'info',
556
+ title: 'Stale gap',
557
+ description: 'A `gaps` item has not been re-checked recently and may be obsolete.',
558
+ remediation: 'Re-check the gap and refresh `last_checked`, or close the gap if it has been resolved.',
559
+ spec_ref: 'UW_FORMAT_SPEC_v1.md §4.22',
560
+ },
561
+ // ─── Integrity (INT-NN) — content_hash / parent_hash chain checks ──────────
562
+ {
563
+ code: 'INT-01', severity: 'error',
564
+ title: 'Parent hash mismatch',
565
+ description: 'A block\'s _meta.parent_hash does not equal the prior block\'s _meta.content_hash in the supersede chain.',
566
+ remediation: 'Recompute the block from the current head: stamp parent_hash with the prior block\'s content_hash and bump the version.',
567
+ spec_ref: 'UW_PROTOCOL_v1.md §V.10',
568
+ },
569
+ {
570
+ code: 'INT-02', severity: 'error',
571
+ title: 'Stale parent hash on edit',
572
+ description: 'applyEdit was invoked with a parent_hash that does not match the current head of the section.',
573
+ remediation: 'Re-read the file, take the latest head\'s content_hash, and retry the edit with the fresh parent_hash.',
574
+ spec_ref: 'UW_PROTOCOL_v1.md §V.10',
575
+ },
576
+ {
577
+ code: 'INT-03', severity: 'warning',
578
+ title: 'Partially hashed chain',
579
+ description: 'Some blocks in a supersede chain carry _meta.content_hash and others do not.',
580
+ remediation: 'Once any block in a chain is hashed, every subsequent block MUST be hashed; rehash the unstamped blocks.',
581
+ spec_ref: 'UW_PROTOCOL_v1.md §V.10',
582
+ },
583
+ {
584
+ code: 'INT-04', severity: 'warning',
585
+ title: 'Content hash does not recompute',
586
+ description: 'A block\'s stamped _meta.content_hash does not match the SHA-256 of its current canonicalized content.',
587
+ remediation: 'Either restore the original content or re-stamp content_hash from the current canonicalized form.',
588
+ spec_ref: 'UW_PROTOCOL_v1.md §V.9',
589
+ },
590
+ // ─── Provenance / policy (POL-NN) — actor and operation authority ──────────
591
+ {
592
+ code: 'POL-01', severity: 'error',
593
+ title: 'Unauthorized actor',
594
+ description: 'The block\'s _meta.actor is not authorized to write this section per its EditPolicy.',
595
+ remediation: 'Re-issue the edit from an actor allowed by the section\'s policy (see BUILTIN_EDIT_POLICIES).',
596
+ spec_ref: 'UW_PROTOCOL_v1.md §VIII',
597
+ },
598
+ {
599
+ code: 'POL-02', severity: 'error',
600
+ title: 'Replace where supersede is required',
601
+ description: 'The section\'s policy requires supersede_on_edit but the head version > 1 has no superseded prior versions.',
602
+ remediation: 'Re-issue the edit as section_supersede so the prior version is preserved as a superseded block.',
603
+ spec_ref: 'UW_PROTOCOL_v1.md §VIII',
604
+ },
605
+ // ─── Financial validity (FV-NN) — renamed from FV_* in v1.1 ────────────────
606
+ // Severity is the *highest* severity any emission of this code can carry
607
+ // (e.g. FV-04 may be warning OR error depending on threshold).
608
+ {
609
+ code: 'FV-01', severity: 'warning',
610
+ title: 'Cap rate below threshold',
611
+ description: 'Going-in cap rate is below the configured warning threshold.',
612
+ remediation: 'Verify NOI and purchase price; an unusually low cap rate often indicates an aggressive valuation.',
613
+ spec_ref: '§5.2',
614
+ },
615
+ {
616
+ code: 'FV-02', severity: 'warning',
617
+ title: 'Cap rate above threshold',
618
+ description: 'Going-in cap rate is above the configured warning threshold.',
619
+ remediation: 'A high cap rate may indicate distressed pricing or market dislocation; confirm the underwriting story.',
620
+ spec_ref: '§5.2',
621
+ },
622
+ {
623
+ code: 'FV-03', severity: 'warning',
624
+ title: 'Debt yield below threshold',
625
+ description: 'Debt yield is below the configured warning threshold.',
626
+ remediation: 'Re-check NOI and loan amount; consider a smaller loan or higher equity.',
627
+ spec_ref: '§5.2',
628
+ },
629
+ {
630
+ code: 'FV-04', severity: 'error',
631
+ title: 'DSCR below threshold',
632
+ description: 'DSCR is below the configured threshold (warning or error).',
633
+ remediation: 'Re-size the loan, lower the rate assumption, or increase NOI to meet the lender constraint.',
634
+ spec_ref: '§5.2',
635
+ },
636
+ {
637
+ code: 'FV-05', severity: 'warning',
638
+ title: 'Equity multiple below minimum',
639
+ description: 'Levered equity multiple is below the configured minimum.',
640
+ remediation: 'Re-examine hold period, exit assumptions, and capital structure.',
641
+ spec_ref: '§5.2',
642
+ },
643
+ {
644
+ code: 'FV-06', severity: 'warning',
645
+ title: 'Equity multiple above maximum',
646
+ description: 'Levered equity multiple is above the configured maximum (likely unrealistic).',
647
+ remediation: 'Sanity-check exit cap, rent growth, and hold-period assumptions.',
648
+ spec_ref: '§5.2',
649
+ },
650
+ {
651
+ code: 'FV-07', severity: 'warning',
652
+ title: 'IRR below threshold',
653
+ description: 'Levered IRR is below the configured warning threshold.',
654
+ remediation: 'Verify the projected exit value and cash-flow trajectory.',
655
+ spec_ref: '§5.2',
656
+ },
657
+ {
658
+ code: 'FV-08', severity: 'warning',
659
+ title: 'IRR above threshold',
660
+ description: 'Levered IRR is above the configured warning threshold (likely unrealistic).',
661
+ remediation: 'An IRR over the upper threshold usually signals an aggressive exit cap or rent-growth assumption.',
662
+ spec_ref: '§5.2',
663
+ },
664
+ {
665
+ code: 'FV-09', severity: 'error',
666
+ title: 'LTV above threshold',
667
+ description: 'LTV is above the configured threshold (warning or error).',
668
+ remediation: 'Reduce loan size or increase appraised value support.',
669
+ spec_ref: '§5.2',
670
+ },
671
+ {
672
+ code: 'FV-10', severity: 'warning',
673
+ title: 'OpEx ratio below minimum',
674
+ description: 'OpEx as a share of EGI is suspiciously low.',
675
+ remediation: 'Verify that all operating line items are captured (taxes, insurance, management, R&M, payroll, utilities).',
676
+ spec_ref: '§5.2',
677
+ },
678
+ {
679
+ code: 'FV-11', severity: 'warning',
680
+ title: 'OpEx ratio above maximum',
681
+ description: 'OpEx as a share of EGI is unusually high.',
682
+ remediation: 'Investigate one-time items, deferred maintenance, or below-market rents.',
683
+ spec_ref: '§5.2',
684
+ },
685
+ {
686
+ code: 'FV-12', severity: 'warning',
687
+ title: 'Annual rent growth above threshold',
688
+ description: 'Annual rent growth assumption exceeds the configured threshold.',
689
+ remediation: 'Tie rent growth to a published submarket forecast or document the rationale.',
690
+ spec_ref: '§5.2',
691
+ },
692
+ {
693
+ code: 'FV-13', severity: 'warning',
694
+ title: 'Vacancy below minimum',
695
+ description: 'Vacancy assumption is below the configured floor.',
696
+ remediation: 'Use the higher of submarket stabilized vacancy or institutional minimum.',
697
+ spec_ref: '§5.2',
698
+ },
699
+ {
700
+ code: 'FV-14', severity: 'warning',
701
+ title: 'Vacancy above maximum',
702
+ description: 'Vacancy assumption is above the configured warning threshold.',
703
+ remediation: 'A vacancy over the upper threshold usually indicates lease-up or distress; re-examine the value-creation story.',
704
+ spec_ref: '§5.2',
705
+ },
706
+ {
707
+ code: 'UNSUPPORTED_YAML_FEATURE', severity: 'error',
708
+ title: 'Unsupported YAML feature in frontmatter',
709
+ description: 'Frontmatter uses a YAML feature outside the .uw.md subset (anchors, tags, block scalars, complex keys, or directives).',
710
+ remediation: 'Rewrite the frontmatter using only the YAML subset documented in UW_FORMAT_SPEC_v1.md Appendix A — scalars, simple mappings, and dash-prefixed sequences.',
711
+ spec_ref: 'UW_FORMAT_SPEC_v1.md Appendix A',
712
+ },
713
+ ]);
714
+ //# sourceMappingURL=protocol.js.map