@uwmd/core 2.11.0 → 2.12.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.
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -1
- package/dist/validator.d.ts +13 -0
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +288 -0
- package/dist/validator.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/waterfall.d.ts +37 -0
- package/dist/waterfall.d.ts.map +1 -1
- package/dist/waterfall.js +67 -1
- package/dist/waterfall.js.map +1 -1
- package/package.json +2 -2
package/dist/validator.js
CHANGED
|
@@ -1093,6 +1093,227 @@ function checkLeasingCapital(t, issues, at) {
|
|
|
1093
1093
|
}
|
|
1094
1094
|
}
|
|
1095
1095
|
}
|
|
1096
|
+
// ─── §4.3 Expense recoveries and the CAM true-up (RFC 0058) ──────────────────
|
|
1097
|
+
//
|
|
1098
|
+
// Recovery income is the second-largest line in most commercial deals, and the
|
|
1099
|
+
// tenant record carried a lease type and one orphan `cam_cap_pct`. These rules
|
|
1100
|
+
// type the terms and check the one piece of arithmetic genuinely knowable from
|
|
1101
|
+
// a single document: a closed period's reconciliation.
|
|
1102
|
+
//
|
|
1103
|
+
// Two things are deliberately NOT recomputed. The cap amount, because a
|
|
1104
|
+
// cumulative or compounding cap depends on a base-year history no single
|
|
1105
|
+
// document carries — REC-07 checks the direction instead, which is the honest
|
|
1106
|
+
// half. And the pool allocation across tenants, because that is a modeling
|
|
1107
|
+
// decision with a policy for vacant space, not a recorded fact; each tenant
|
|
1108
|
+
// states its own share.
|
|
1109
|
+
/** Recovery methods. Closed — a producer meaning something else states `none`. */
|
|
1110
|
+
export const RECOVERY_METHODS = Object.freeze([
|
|
1111
|
+
'net', 'base_year_stop', 'fixed_stop', 'fixed_amount', 'none',
|
|
1112
|
+
]);
|
|
1113
|
+
/**
|
|
1114
|
+
* The §4.4 `operating_statement.expenses` keys a pool may draw from.
|
|
1115
|
+
*
|
|
1116
|
+
* Deliberately excludes `management_fee_pct_egi` (a ratio, not an expense),
|
|
1117
|
+
* `capital_expenditures_actual` and `replacement_reserves` (capital, not
|
|
1118
|
+
* operating), and `total_operating_expenses` (a total — naming it would double
|
|
1119
|
+
* every member beside it).
|
|
1120
|
+
*/
|
|
1121
|
+
export const RECOVERABLE_EXPENSE_KEYS = Object.freeze([
|
|
1122
|
+
'real_estate_taxes', 'insurance', 'management_fees', 'payroll_benefits',
|
|
1123
|
+
'utilities', 'repairs_maintenance', 'contract_services',
|
|
1124
|
+
'marketing_advertising', 'administrative', 'professional_fees',
|
|
1125
|
+
'other_expenses',
|
|
1126
|
+
]);
|
|
1127
|
+
export const RECOVERY_CAP_ACCUMULATIONS = Object.freeze([
|
|
1128
|
+
'cumulative', 'non_cumulative', 'compounding',
|
|
1129
|
+
]);
|
|
1130
|
+
export const RECOVERY_SETTLEMENTS = Object.freeze([
|
|
1131
|
+
'billed', 'credited', 'disputed', 'unsettled',
|
|
1132
|
+
]);
|
|
1133
|
+
/** Currency quantum, matching every sibling verifier's reporting boundary. */
|
|
1134
|
+
const RECOVERY_DP = 2;
|
|
1135
|
+
function recIssue(issues, code, field, message, value, severity = 'error') {
|
|
1136
|
+
issues.push({
|
|
1137
|
+
code, severity, section: 'rent_roll', field, message,
|
|
1138
|
+
...(value !== undefined ? { value } : {}),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
function roundRec(value) {
|
|
1142
|
+
const f = 10 ** RECOVERY_DP;
|
|
1143
|
+
const scaled = value * f;
|
|
1144
|
+
return (scaled < 0 ? -Math.round(-scaled) : Math.round(scaled)) / f;
|
|
1145
|
+
}
|
|
1146
|
+
function checkRecoveryTerms(t, issues, at) {
|
|
1147
|
+
const raw = t['recovery_terms'];
|
|
1148
|
+
if (raw == null)
|
|
1149
|
+
return null;
|
|
1150
|
+
const p = `${at}.recovery_terms`;
|
|
1151
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1152
|
+
recIssue(issues, 'REC-02', p, 'REC-02: recovery_terms must be an object stating a method', raw);
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
const terms = raw;
|
|
1156
|
+
// REC-01: the share is a fraction, not a percent. 4.12 instead of 0.0412 is
|
|
1157
|
+
// the single most consequential typo available here — it multiplies every
|
|
1158
|
+
// recovery by a hundred.
|
|
1159
|
+
const share = terms['pro_rata_share'];
|
|
1160
|
+
let shareValue = null;
|
|
1161
|
+
if (share != null) {
|
|
1162
|
+
if (!(leaseNum(share) && share > 0 && share <= 1)) {
|
|
1163
|
+
recIssue(issues, 'REC-01', `${p}.pro_rata_share`, 'REC-01: pro_rata_share must be a fraction in (0,1] — 0.0412 is 4.12%, not 4.12', share);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
shareValue = share;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
// REC-02: a method without the input it needs.
|
|
1170
|
+
const method = terms['method'];
|
|
1171
|
+
if (typeof method !== 'string' || !RECOVERY_METHODS.includes(method)) {
|
|
1172
|
+
recIssue(issues, 'REC-02', `${p}.method`, `REC-02: method must be one of ${RECOVERY_METHODS.join(', ')} — the vocabulary is closed`, method);
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
const REQUIRED = {
|
|
1176
|
+
base_year_stop: 'base_year',
|
|
1177
|
+
fixed_stop: 'expense_stop_per_sqft',
|
|
1178
|
+
fixed_amount: 'fixed_recovery_annual',
|
|
1179
|
+
};
|
|
1180
|
+
const needs = REQUIRED[method];
|
|
1181
|
+
if (needs && terms[needs] == null) {
|
|
1182
|
+
recIssue(issues, 'REC-02', `${p}.${needs}`, `REC-02: method "${method}" requires ${needs}; without it the method states nothing`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
// REC-03: a pool entry that names nothing recovers nothing, silently.
|
|
1186
|
+
const pool = terms['recoverable_pool'];
|
|
1187
|
+
if (pool != null) {
|
|
1188
|
+
if (!Array.isArray(pool) || pool.length === 0) {
|
|
1189
|
+
recIssue(issues, 'REC-03', `${p}.recoverable_pool`, 'REC-03: recoverable_pool must be a nonempty array of operating-statement expense keys', pool);
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
for (const [j, entry] of pool.entries()) {
|
|
1193
|
+
if (typeof entry !== 'string' || !RECOVERABLE_EXPENSE_KEYS.includes(entry)) {
|
|
1194
|
+
recIssue(issues, 'REC-03', `${p}.recoverable_pool[${j}]`, `REC-03: ${JSON.stringify(entry)} is not a recoverable operating_statement.expenses key — a pool naming a nonexistent expense recovers zero without saying so`, entry);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
// The cap. Accumulation has no default: the three treatments diverge
|
|
1200
|
+
// materially within three years, so a silent choice is a wrong number.
|
|
1201
|
+
const cap = terms['cap'];
|
|
1202
|
+
if (cap != null) {
|
|
1203
|
+
const cp = `${p}.cap`;
|
|
1204
|
+
if (typeof cap !== 'object' || Array.isArray(cap)) {
|
|
1205
|
+
recIssue(issues, 'REC-02', cp, 'REC-02: cap must be an object', cap);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
const c = cap;
|
|
1209
|
+
const pct = c['pct'];
|
|
1210
|
+
if (pct != null && !(leaseNum(pct) && pct > 0 && pct < 1)) {
|
|
1211
|
+
recIssue(issues, 'REC-01', `${cp}.pct`, 'REC-01: cap.pct must be a fraction in (0,1) — 0.05 is 5%, not 5', pct);
|
|
1212
|
+
}
|
|
1213
|
+
if (pct != null) {
|
|
1214
|
+
const acc = c['accumulation'];
|
|
1215
|
+
if (typeof acc !== 'string' || !RECOVERY_CAP_ACCUMULATIONS.includes(acc)) {
|
|
1216
|
+
recIssue(issues, 'REC-02', `${cp}.accumulation`, `REC-02: a stated cap.pct requires cap.accumulation (${RECOVERY_CAP_ACCUMULATIONS.join(' | ')}) — there is no default, because the three disagree`, acc);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// REC-10: the legacy field and the typed cap can contradict each other.
|
|
1222
|
+
if (t['cam_cap_pct'] != null && cap != null) {
|
|
1223
|
+
recIssue(issues, 'REC-10', `${at}.cam_cap_pct`, 'REC-10: cam_cap_pct is superseded by recovery_terms.cap; stating both lets the two drift apart', t['cam_cap_pct'], 'warning');
|
|
1224
|
+
}
|
|
1225
|
+
return shareValue;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* REC-09. A settled true-up becomes a dated cash line in §4.26 — the
|
|
1229
|
+
* cross-cutting requirement that new dated cash lands in the addressable sink,
|
|
1230
|
+
* where assembly and receipt coverage already verify it. A reference that
|
|
1231
|
+
* resolves to nothing is the one failure mode that looks like success: the row
|
|
1232
|
+
* claims the amount reached the cash flows, and nothing checks that it did.
|
|
1233
|
+
*/
|
|
1234
|
+
function recoveryRefResolves(parsed, variant) {
|
|
1235
|
+
const entry = parsed.sections['cash_flow_series'];
|
|
1236
|
+
if (!entry)
|
|
1237
|
+
return false;
|
|
1238
|
+
return isVariantMap(entry)
|
|
1239
|
+
? entry[variant] !== undefined
|
|
1240
|
+
: variant === 'default';
|
|
1241
|
+
}
|
|
1242
|
+
function checkRecoveryTrueUp(t, issues, at, share, asOf, parsed) {
|
|
1243
|
+
const rows = t['recovery_true_up'];
|
|
1244
|
+
if (rows == null)
|
|
1245
|
+
return;
|
|
1246
|
+
if (!Array.isArray(rows)) {
|
|
1247
|
+
recIssue(issues, 'REC-04', `${at}.recovery_true_up`, 'REC-04: recovery_true_up must be an array of reconciliation rows', rows);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
for (const [j, raw] of rows.entries()) {
|
|
1251
|
+
const p = `${at}.recovery_true_up[${j}]`;
|
|
1252
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1253
|
+
recIssue(issues, 'REC-04', p, 'REC-04: each true-up entry must be an object', raw);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const row = raw;
|
|
1257
|
+
const start = row['period_start'];
|
|
1258
|
+
const end = row['period_end'];
|
|
1259
|
+
if (!isDate(start) || !isDate(end)) {
|
|
1260
|
+
recIssue(issues, 'REC-04', `${p}.period_start`, 'REC-04: a true-up states period_start and period_end as YYYY-MM-DD dates');
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (end < start) {
|
|
1264
|
+
recIssue(issues, 'REC-04', `${p}.period_end`, `REC-04: period_end (${end}) precedes period_start (${start})`, end);
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
// REC-05: a reconciliation of a period that has not ended is a forecast.
|
|
1268
|
+
// Anchored on the rent roll's own as_of_date — never on file metadata,
|
|
1269
|
+
// which is an edit timestamp and would refuse a legitimately re-saved
|
|
1270
|
+
// document. Skipped when the rent roll states no date.
|
|
1271
|
+
if (asOf !== null && !(end < asOf)) {
|
|
1272
|
+
recIssue(issues, 'REC-05', `${p}.period_end`, `REC-05: the true-up period ends ${end}, on or after the rent roll as_of_date ${asOf} — a reconciliation of an open period is a forecast, and this section carries settled facts`, end);
|
|
1273
|
+
}
|
|
1274
|
+
const poolActual = row['pool_actual'];
|
|
1275
|
+
const uncapped = row['tenant_share_uncapped'];
|
|
1276
|
+
const capped = row['tenant_share_capped'];
|
|
1277
|
+
const billed = row['estimated_billed'];
|
|
1278
|
+
const trueUp = row['true_up_amount'];
|
|
1279
|
+
// REC-06: the one product a single document can check.
|
|
1280
|
+
if (leaseNum(poolActual) && share !== null && leaseNum(uncapped)) {
|
|
1281
|
+
const want = roundRec(poolActual * share);
|
|
1282
|
+
if (roundRec(uncapped) !== want) {
|
|
1283
|
+
recIssue(issues, 'REC-06', `${p}.tenant_share_uncapped`, `REC-06: tenant_share_uncapped states ${uncapped} but pool_actual x pro_rata_share is ${want}`, uncapped);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
// REC-07: the cap can only reduce. The capped amount itself is stated,
|
|
1287
|
+
// not recomputed — see the section note.
|
|
1288
|
+
if (leaseNum(uncapped) && leaseNum(capped) && roundRec(capped) > roundRec(uncapped)) {
|
|
1289
|
+
recIssue(issues, 'REC-07', `${p}.tenant_share_capped`, `REC-07: tenant_share_capped (${capped}) exceeds tenant_share_uncapped (${uncapped}) — a cap cannot increase a recovery`, capped);
|
|
1290
|
+
}
|
|
1291
|
+
// REC-08: the subtraction the whole row exists to record.
|
|
1292
|
+
if (leaseNum(capped) && leaseNum(billed) && leaseNum(trueUp)) {
|
|
1293
|
+
const want = roundRec(capped - billed);
|
|
1294
|
+
if (roundRec(trueUp) !== want) {
|
|
1295
|
+
recIssue(issues, 'REC-08', `${p}.true_up_amount`, `REC-08: true_up_amount states ${trueUp} but tenant_share_capped less estimated_billed is ${want}`, trueUp);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const settlement = row['settlement'];
|
|
1299
|
+
if (settlement != null && !(typeof settlement === 'string' && RECOVERY_SETTLEMENTS.includes(settlement))) {
|
|
1300
|
+
recIssue(issues, 'REC-04', `${p}.settlement`, `REC-04: settlement must be one of ${RECOVERY_SETTLEMENTS.join(', ')}`, settlement);
|
|
1301
|
+
}
|
|
1302
|
+
// REC-09: the §4.26 handoff.
|
|
1303
|
+
const ref = row['cash_flow_ref'];
|
|
1304
|
+
if (ref != null) {
|
|
1305
|
+
const variant = typeof ref === 'object' && !Array.isArray(ref)
|
|
1306
|
+
? ref['variant']
|
|
1307
|
+
: undefined;
|
|
1308
|
+
if (typeof variant !== 'string' || variant.length === 0) {
|
|
1309
|
+
recIssue(issues, 'REC-09', `${p}.cash_flow_ref`, 'REC-09: cash_flow_ref must name a cash_flow_series variant', ref);
|
|
1310
|
+
}
|
|
1311
|
+
else if (!recoveryRefResolves(parsed, variant)) {
|
|
1312
|
+
recIssue(issues, 'REC-09', `${p}.cash_flow_ref.variant`, `REC-09: cash_flow_ref.variant ${JSON.stringify(variant)} does not resolve to a cash_flow_series variant in this document — the settled amount claims a cash line that is not there`, variant);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1096
1317
|
function checkLeaseClauses(parsed, issues) {
|
|
1097
1318
|
const block = resolveCrossCheckSection(parsed, 'rent_roll').block;
|
|
1098
1319
|
if (!block)
|
|
@@ -1100,6 +1321,8 @@ function checkLeaseClauses(parsed, issues) {
|
|
|
1100
1321
|
const tenants = deepGet(block.content, 'tenants');
|
|
1101
1322
|
if (!Array.isArray(tenants))
|
|
1102
1323
|
return;
|
|
1324
|
+
const rawAsOf = deepGet(block.content, 'as_of_date');
|
|
1325
|
+
const asOf = isDate(rawAsOf) ? rawAsOf : null;
|
|
1103
1326
|
for (const [i, raw] of tenants.entries()) {
|
|
1104
1327
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
1105
1328
|
continue;
|
|
@@ -1109,6 +1332,8 @@ function checkLeaseClauses(parsed, issues) {
|
|
|
1109
1332
|
checkTerminationOption(t, issues, at);
|
|
1110
1333
|
checkCoTenancy(t, issues, at);
|
|
1111
1334
|
checkLeasingCapital(t, issues, at);
|
|
1335
|
+
const share = checkRecoveryTerms(t, issues, at);
|
|
1336
|
+
checkRecoveryTrueUp(t, issues, at, share, asOf, parsed);
|
|
1112
1337
|
}
|
|
1113
1338
|
}
|
|
1114
1339
|
// ─── §4.7 / §4.8 Rate hedges and escrows (RFC 0056) ──────────────────────────
|
|
@@ -2066,6 +2291,54 @@ function sumsToOne(a, b) {
|
|
|
2066
2291
|
a >= 0 && a <= 1 && b >= 0 && b <= 1 &&
|
|
2067
2292
|
Math.abs(a + b - 1) < RATIO_QUANTUM;
|
|
2068
2293
|
}
|
|
2294
|
+
/**
|
|
2295
|
+
* RFC 0059. The clawback is a terminal true-up, so the only structural
|
|
2296
|
+
* questions are whether its basis carries the input it needs, whether the cap
|
|
2297
|
+
* is the one cap that exists, and whether the rates are fractions.
|
|
2298
|
+
*/
|
|
2299
|
+
function checkWaterfallClawback(content, tiers, section, label, issues) {
|
|
2300
|
+
const raw = content['clawback'];
|
|
2301
|
+
if (raw == null)
|
|
2302
|
+
return;
|
|
2303
|
+
const push = (code, field, message) => {
|
|
2304
|
+
issues.push({ code, severity: 'error', section, field: `clawback.${field}`, message: `${code}: ${message}${label}` });
|
|
2305
|
+
};
|
|
2306
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
2307
|
+
push('WF-10', 'basis', 'clawback must be an object stating a basis and a cap');
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
const cb = raw;
|
|
2311
|
+
const basis = cb['basis'];
|
|
2312
|
+
const BASES = ['lp_preferred_shortfall', 'lp_irr_floor', 'lp_em_floor'];
|
|
2313
|
+
if (typeof basis !== 'string' || !BASES.includes(basis)) {
|
|
2314
|
+
push('WF-10', 'basis', `basis must be one of ${BASES.join(', ')} — the vocabulary is closed`);
|
|
2315
|
+
}
|
|
2316
|
+
else if (basis === 'lp_irr_floor') {
|
|
2317
|
+
const rate = cb['floor_rate'];
|
|
2318
|
+
if (!(typeof rate === 'number' && Number.isFinite(rate) && rate > 0 && rate < 1)) {
|
|
2319
|
+
push('WF-10', 'floor_rate', 'lp_irr_floor requires floor_rate as a fraction in (0,1) — 0.12 is 12%, not 12');
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
else if (basis === 'lp_em_floor') {
|
|
2323
|
+
const mult = cb['floor_multiple'];
|
|
2324
|
+
if (!(typeof mult === 'number' && Number.isFinite(mult) && mult > 1)) {
|
|
2325
|
+
push('WF-10', 'floor_multiple', 'lp_em_floor requires floor_multiple greater than 1');
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
// WF-11: a preferred-shortfall floor with no preferred tier to measure it.
|
|
2329
|
+
if (basis === 'lp_preferred_shortfall' && !tiers.some((t) => t?.type === 'preferred_return')) {
|
|
2330
|
+
push('WF-11', 'basis', 'lp_preferred_shortfall needs a preferred_return tier — without one there is no accrual to fall short of');
|
|
2331
|
+
}
|
|
2332
|
+
// WF-12: the cap is not a choice.
|
|
2333
|
+
if (cb['cap'] !== 'promote_received') {
|
|
2334
|
+
push('WF-12', 'cap', 'cap must be "promote_received" — a GP cannot owe back more promote than it received, and any other cap is a different instrument');
|
|
2335
|
+
}
|
|
2336
|
+
// WF-13: a stated tax rate is a fraction.
|
|
2337
|
+
const tax = cb['net_of_tax_rate'];
|
|
2338
|
+
if (tax != null && !(typeof tax === 'number' && Number.isFinite(tax) && tax >= 0 && tax < 1)) {
|
|
2339
|
+
push('WF-13', 'net_of_tax_rate', 'net_of_tax_rate must be a fraction in [0,1) — 0.37 is 37%, not 37');
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2069
2342
|
function checkWaterfallContent(content, variant, parsed, issues) {
|
|
2070
2343
|
const section = 'distribution_waterfall';
|
|
2071
2344
|
const label = variant === 'default' ? '' : ` (variant "${variant}")`;
|
|
@@ -2193,6 +2466,8 @@ function checkWaterfallContent(content, variant, parsed, issues) {
|
|
|
2193
2466
|
wf01('tiers', 'the ladder must end in at least one split tier');
|
|
2194
2467
|
}
|
|
2195
2468
|
}
|
|
2469
|
+
// WF-10 .. WF-15: the RFC 0059 clawback provision.
|
|
2470
|
+
checkWaterfallClawback(content, rows, section, label, issues);
|
|
2196
2471
|
// WF-02 / WF-03: the cash reference.
|
|
2197
2472
|
const ref = content['cash_flow_ref'];
|
|
2198
2473
|
const refVariant = ref && typeof ref['variant'] === 'string' ? ref['variant'] : null;
|
|
@@ -2224,6 +2499,19 @@ function checkWaterfallContent(content, variant, parsed, issues) {
|
|
|
2224
2499
|
message: `WF-03: the referenced series${label} has no contribution (no negative amount); a waterfall over pure inflows has no capital to return`,
|
|
2225
2500
|
});
|
|
2226
2501
|
}
|
|
2502
|
+
// WF-15 (RFC 0059): a clawback provision is unexercisable without a terminal
|
|
2503
|
+
// distribution to have overpaid out of. A warning, not an error — the
|
|
2504
|
+
// provision may legitimately be stated ahead of the exit.
|
|
2505
|
+
if (content['clawback'] != null && Array.isArray(seriesRows)) {
|
|
2506
|
+
const lastPositive = [...seriesRows].reverse().find((r) => r && typeof r === 'object' && typeof r['amount'] === 'number' &&
|
|
2507
|
+
r['amount'] > 0);
|
|
2508
|
+
if (lastPositive === undefined) {
|
|
2509
|
+
issues.push({
|
|
2510
|
+
code: 'WF-15', severity: 'warning', section, field: 'clawback',
|
|
2511
|
+
message: `WF-15: a clawback provision is stated but the referenced series${label} has no distribution, so no promote can have been paid and the provision is unexercisable as stated`,
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2227
2515
|
}
|
|
2228
2516
|
function checkWaterfall(parsed, issues) {
|
|
2229
2517
|
const entry = parsed.sections['distribution_waterfall'];
|