Sooner · Underwriting Resolution Contract← Decision Register
Contents1. Decisions Table2. Reframed / Resolved Findings — The Answers to “Elaborate” and “What Do You Suggest”3. Change Specs — Concrete Edits3.1 Honest score — re-weight + visible override, NO cap (S-a/S-b, SIMPLIFIED 2026-06-08)3.2 CATS re-weighted, sum to 100, J RETAINED (S-i / D1, revised 2026-06-08)3.3 Employer + recourse are sum-of-parts, NOT double-counted (S-c/S-d, REFRAMED 2026-06-08)3.4 Band interpolation (S-j)3.5 decide() rewrite (S-a/S-b/S-e/R10, S-c, S-d)3.6 Employer recalibration table (S-f — FIXED, drop-in)3.7 Income haircuts (S-h)3.8 DBR from primitives (G9 / R9)3.9 Hard vs soft reject + path-to-acceptance (G-k / G6 / D7, formalised 2026-06-08)3.10 Gate fixes (G2, G3, G8)3.11 Thin-file substitute = average-to-good (S-g — DECIDED 2026-06-08, REVERSED)3.12 New affordability gates (F-f / R18)3.13 Recourse-ranking rubric v2 — on actual DLD rental lease data (R-b/R-c/R-e/R-f)3.14 Void-driven recovery (L6 / F-e — REFRAMED 2026-06-08)3.15 Concentration updates — post-2026-correction (R-h / Gov-g)3.16 Fair-lending monitoring pack (Gov-e)4. New Artifacts — Ready to Build4.1 Expected-Loss Model (resolves C6 / L5 / R11 / R17)4.2 Key Facts Statement (resolves C3 / R15)5. Canonical Page + Forms-Embedding Architecture5.1 Four-layer restructure of apps/underwriting-criteria/5.2 Field-to-gate map (forms embed)5.3 Where each check runs (progressive disclosure — run a gate at the earliest step all its inputs exist)5.4 Build sequence6. Open Items — Genuinely Need Data / Calibration / Legal / Founder Input

Paths confirmed. The engine apply forms exist at sooner-underwriting-engine/apps/web/pages/apply/ (lead/index/preapproval/result/documents.tsx — note preapproval.tsx not index.tsx is the main form, and there’s no result.tsx listed wait it is). I have enough to write the contract. The cluster outputs are self-contained; I’ll consolidate now without re-reading the 145KB index.html (the clusters already cite exact line numbers).

Here is the canonical document.


Sooner Underwriting — Canonical Resolution & Implementation Contract

Version 1.0 · Compiled 2026-06-07 · Owner: Gabriel Garcia Leyva (lead underwriter) · Implementer: engine + site team

This is the single source of truth for every finding raised against the Sooner underwriting engine and reference site. It supersedes the four cluster outputs. Section 3 (change specs), Section 4 (new artifacts), and Section 5 (architecture) are implementation-ready. Section 6 lists what still needs founder/data/legal input.

Target files referenced throughout: - Reference site: /Users/gabo/Desktop/Projects/sooner-mini-apps/apps/underwriting-criteria/index.html - Engine forms: /Users/gabo/Desktop/Projects/sooner-underwriting-engine/apps/web/pages/apply/{lead,preapproval,index,documents,result}.tsx - Data dictionary: /Users/gabo/Desktop/Projects/sooner-mini-apps/apps/data-dictionary/ - Rent-vs-buy benchmark: https://sooner-rent-vs-buy.pages.dev/


1. Decisions Table

Disposition legend: fixed = recalibration already produced, drop-in ready · resolved = answered by reframe or existing artifact, no/cosmetic logic change · reframed = critique was wrong or mis-scoped; answer documented · planned = real defect, change spec written, not yet coded · open = genuinely needs data/calibration/legal/founder input.

Dimension: Pricing / Economics / Disclosure

ID Disposition One-line resolution
P2 reframed Margin is strongly positive (ROE 53.4%, gross IRR 41.5%, net IRR 22.4%, NIM 12.15%); OID commissions cut net capital to ~112,600 and the bundle clears 15% CoF by 7–22pts.
P3 reframed Flat pricing is not circular once the EL model is built independently; premium validated at 22.4% net IRR after CoF + opex + EL.
P4 reframed Owner never loses equity/title; Sooner’s recourse is the master lease, recovers from re-lease rent then exits — structurally opposite of equity-stripping.
P5 reframed Buyer pays nothing extra for Home Discovery/Brokerage; seller pays 21k commission, bank pays 16k referral — bundle lowers buyer price via third-party revenue.
P7 resolved Total-cost benchmark already exists: the rent-vs-buy calculator. Link it from Pricing + KFS + topnav.
P8 planned Engine implements no pricing/EL/disclosure today; port soonerMonthly/effectiveAPR/bundle-select into the engine, add expectedLoss(), emit KFS.
F-b / C7 planned Affordability tested once → add continuous Lean monitor + annual AECB re-pull + savings gate + stressed-DBR.
F-c reframed High effective APR is real and disclosed, but appreciation (~+5%/yr) + avoided rent net the entry cost positive; show value framing after the mandatory APR disclosure.
F-d resolved Early-termination schedule exists (6%→1% declining, 6-month floor), Shariah actual-loss framed; surface on Pricing + KFS, no logic change.
F-g reframed Product necessity validated: personal loan breaches 50% DBR, card costs more, wait-and-save forgoes appreciation.
C3 / R15 planned KFS content written (Section 4); build as .docx + in-app tab, driven by engine constants.
C6 / L5 / R11 / R17 planned EL model spec written (Section 4); reconciles 4% PD vs 1% provision (see Section 6).

Dimension: Scoring Engine

ID Disposition One-line resolution
S-a decided (D1) SIMPLIFIED 2026-06-08: no capacity cap. Re-weight the key levers (employment 15→24, re-leasing 10→15; §3.2), keep J in the additive pool, and expose the override as published reason codes (§3.1). Score = honest sum-of-parts; vetoes come from gates + the D5 weak+weak rule.
S-b planned Score is decorative for capped segments → relabel score “one input”, publish the waterfall as the decision surface; 70/55 cutoffs apply only when no cap binds.
S-c planned Employer tier triple-counted (J score + G7 + waterfall) → de-dup to exactly two loci: G7 tenure + decision-band cap/factor.
S-d planned Recourse tier counted 4× → de-dup to three distinct questions: G12 eligibility, G15 coverage, Category H + recourse cap.
S-e / R10-affordability-floor planned R10 recourse-rescue has no affordability floor → require raw.A≥82 AND raw.B≥82 (DBR≤45% + 6mo buffer) before recourse can rescue a weak employer. Same change, implement once.
S-f fixed Employer model recalibrated across 6 axes (Section 3.6); closes R2 (ADNOC→T1).
S-g planned Thin-file substitute too generous (62) → drop to 35 conservative floor.
S-h planned Income haircuts set: additional 50%, commission 50% (rental 70% per one cluster variant — reconcile, Section 6).
S-i planned CATS weights sum to 105 → rebase to 100 after removing J as an additive slot.
S-j planned 4-rung ladder creates cliffs/dead-zones → piecewise-linear interpolation between published band breakpoints.
G-k / G6↔︎G9 planned 25k income floor lets structurally-unaffordable files pass G6 then fail G9 → compute requiredIncomeFloor = (mortgage + Sooner monthly)/0.50, fail fast.

Dimension: Recourse Ranking & Recovery

ID Disposition One-line resolution
R-b resolved Built on lease-level DLD rental data we hold (every new lease + ~50% renewals; price/duration/unit-type/beds/building/location). Depth = new-lease registration count (primary); rent = actual median registered lease price; turnover/void from lease duration + renewal gaps; sales volume = tie-breaker only; reports for supply. Dated manual-override register on top; join by PSL code. Our own measured void days layered in as re-leases accumulate (3.13).
R-c reframed Yield sign is wrong in the tail → score net-of-void achievable yield as an inverted-U (peak 5.5–7%, penalize >8.5%).
R-d open Ranking still render-only; engine takes tier as a set input. Build resolveRecourseTier() + wire R10 + emit audit signals — AFTER rubric ratified.
R-e planned Cut-offs/weights uncalibrated → 4-stage staged calibration, re-baseline off the post-Iran-shock window, back-test at 30–50 realised re-leases.
R-f reframed/resolved Ranking must key off rental-recovery primitives, not sales volume; subsumed by R-b. Feed same voidDays/net-rent into recoveryModel() and provision.
R-h reconciled Concentration policy exists AND is reconciled with the community-tiering artifact: community-classification.json v2026-04-30 (tier A/B/C + cluster per community) is the canonical tiering source; Section 3.15 cluster caps reference the SAME clusters. Cluster membership = the classification file; cap numbers = 3.15 (pending Gabriel ratify, R22). No separate concentration doc owed.
L6 planned Owner-occupier vs investor recovery undifferentiated → add propertyUsage, longer carry + Cat H haircut for investor_tenanted.
F-e reframed Displacement critique mis-reads recovery as frictionless; the ~2–4mo carry + void is already priced; owner regains control on recovery.
Gov-f resolved Named Section-A carve-outs (Hartland Greens=B, Wilton Park=B, The Crest=C) override the aggregate “Sobha Hartland” ranking row; encode precedence.

Dimension: Governance / Gates / Ops

ID Disposition One-line resolution
Gov-e planned No demographic capture → monitoring-only, hard-walled capture + adverse-action reason codes + quarterly AIR<0.80 test + champion-challenger.
Gov-g resolved Concentration policy exists but lives in docs → encode CONCENTRATION + checkConcentration() as code; apply R-h updates.
Gov-h reframed/resolved BNPL via Lean bank-statements = sufficient (often better than AECB); add as DBR input. PDPL = a consent/retention compliance workstream, not a bank-statement check.
G2 planned Age check is origination-only → enforce age-at-maturity: age≥21 AND age+5≤65.
G3 planned Thin-file bypasses AECB → AECB pull mandatory; thin-file is set BY the pull result, not a free toggle.
G8 resolved 6–11% range is correct (actual fees, not a fixed %); reframe as guardrail and drop absolute floor 50k→30k.
G9 / R9 planned Compute DBR from primitives with bank mortgage counted once; Category A consumes the same dbrTotal.
G-f resolved RERA rent caps / sitting-occupier protections never bind (eviction is non-payment-only); one clarifying line, no engine change.
F-f / R18 planned All-at-ceiling files can auto-approve → thin-affordability committee trigger + hard savings gate G16 + stressed-DBR check.
Gov-f (Sobha) / R23 resolved Page = canonical spec; engine catches up; split the aggregate Sobha ranking row by sub-community.
R2 resolved ADNOC ratified to T1 (sovereign state-energy), closed by S-f.

Dimension: Canonical Page & Forms Architecture

ID Disposition One-line resolution
canonical-page-design planned Extract inline logic into underwriting-contract.js (versioned, deterministic ES module); page renders FROM it; add EL / KFS / Concentration / Ratification-ledger tabs.
forms-embed-plan planned Publish @sooner/underwriting-contract workspace package; forms run advisory client checks, engine runs the identical module server-side as the binding decision; CI golden-profile parity gate.
R-d (engine parity) open Engine still hard-declines T4/T5 and takes commTier as input; parity ships structurally when engine imports the contract module.

Ratification ledger (needs Gabriel’s sign-off to flip resolved): R2 (ADNOC→T1 ✔ decided), R5 (additional-income haircut 50%), R7 (self-employed→manual), R9 (DBR from primitives), R10 (employer×recourse + affordability floor), R15 (KFS/APR actual-loss framing), R16 (recovery data source), R17 (1%/yr provision — see Section 6), R18 (60mo affordability gates), R19 (score honesty/caps), R20 (calibration + fair-lending + concentration), R21 (fair-lending pack), R22 (concentration re-baseline), R23 (Sobha sub-community reconciliation).


2. Reframed / Resolved Findings — The Answers to “Elaborate” and “What Do You Suggest”

These are the narrative answers, each a tight paragraph, ordered as they surfaced. Drop these onto the canonical page so funders and auditors read the answer in context.

P2 — “the margin barely clears cost of funds.” Wrong as written. The model shows 53.4% annualized ROE, 41.5% gross IRR, 22.4% net IRR, and 12.15% NIM, with ~76,320 five-year net profit per deal. The bridge: on the base case the buyer pays an 80,745 premium (50% of the 161,490 principal, 10%/yr flat). On top of that, OID commissions of ~37,000 (21,000 seller-side sale commission + 16,000 bank referral on the 1.6M mortgage) arrive at origination, cutting net capital deployed from 161,490 to ~112,600 and equity-at-risk to ~28,440. Gross IRR (41.5%) exceeds net IRR (22.4%) precisely because of the 15% facility drag, and it still clears it by 7–22 points. The flat 10%/yr looks thin only if you ignore the OID; the OID is the whole point of the bundle.

P3 — “flat pricing is circular because EL is never computed.” Decoupled once the EL model exists separately. The new EL model computes PD × LGD × EAD independently from the recovery economics, and the premium is checked against it, not derived from it. Reassessed: 10%/yr flat = 18.7% effective APR; subtract a ~12% blended funding cost (15% CoF on 80% debt), ~1%/yr opex, and the ~1–2.4%/yr EL the model produces, and the residual is 22.4% net IRR. Pricing is flat by design (bundle-driven, Shariah-framed lease rental, not risk-priced per deal), and that is defensible because the master-lease recourse compresses LGD dispersion across deals. The EL model proves the flat premium is adequate; it does not set it. Risk-based pricing is deferred until portfolio data calibrates the EL tail.

P4 — “predatory equity-stripping optics.” Factually wrong for this structure. Sooner finances closing fees (~8% of value), not the property. The buyer holds the freehold and the bank mortgage throughout. Sooner’s recourse is a master lease over the unit, not a charge on the buyer’s equity. On default Sooner takes vacant possession via the fast Article 25(1) non-payment route, re-leases to recover ~9–12 months of rent, then EXITS the master lease — the owner regains full possession with mortgage and equity intact. The owner can lose at most the rent Sooner collects during recovery, never their ownership stake. This is structurally the opposite of equity-stripping.

P5 — “the bundle inverts price against the buyer.” It does not. The buyer pays nothing extra for Home Discovery or Mortgage Brokerage. Sooner earns the 21,000 sale commission from the seller (1.05% of 2M) and the 16,000 mortgage referral from the bank (1% of the 1.6M mortgage). The cheaper-looking full bundle (10%/yr vs 15%/yr CFF-only) is cheaper for the buyer because Sooner is being paid by third parties on that deal, not because Sooner is repricing the buyer. The “inversion” critique assumed the buyer funds all three services; they do not. The discount is third-party revenue passing through, which aligns interests rather than inverting them.

F-c — “expensive credit on a sunk cost.” The sunk-cost/high-APR framing ignores the appreciation offset. On a 2M home the financed fee is ~60–161k. Dubai prices have averaged ~+5%/yr, so the home gains ~100k/yr. A buyer who uses Sooner buys now instead of spending 1–2 years saving the closing stack; over that wait they would forgo ~100–200k of appreciation plus pay rent. The premium (~80k over 5 years base case) is smaller than the appreciation captured by buying earlier, before netting avoided rent. The honest framing is “a financed entry that pays for itself in captured appreciation plus avoided rent,” not “expensive credit on a sunk cost.” The 18.7% effective APR is real and must be disclosed (KFS), but APR-in-isolation is the wrong lens for a one-time entry cost that unlocks an appreciating, rent-replacing asset.

F-g — “is this product even necessary?” Validated by the alternatives being strictly worse or blocked. A buyer who lacks the closing cash has three options: (1) a personal loan for ~8% of value, which adds a monthly obligation pushing most buyers over the 50% DBR cap (G9), so the bank declines the mortgage and the purchase dies; (2) a credit card at ~30–40% effective APR (worse than Sooner’s 18.7%) with far shorter tenor; or (3) wait 1–2 years saving, forgoing ~100–200k appreciation. Sooner’s master-lease structure keeps the financed amount tested inside G9 with the Sooner monthly included rather than as a separate bank loan, so it does not blow the mortgage. The product exists because the obvious substitutes either breach the DBR ceiling or cost more.

F-e — “owner-occupier displacement.” The critique mis-reads recovery as a frictionless seizure. The sub-lease ends only for non-payment; Sooner then takes vacant possession via Article 25(1) (~2–4 months) plus a re-lease void — a real cost already carried in the recovery model and the 1%/yr provision. The owner does not lose equity: Sooner re-leases for the remaining term to recover the ~8%-of-value financed amount, then exits and the owner regains control. The only case that looks like displacement is an owner-occupier who has already stopped paying — that is the recovery mechanism functioning, and it is slow and costly enough that it is never the cheap option the critique assumes. The genuinely-slower path is the investor-tenanted case, differentiated per L6.

F-d — “no early-exit / refinance path.” Resolved: an early-termination schedule exists, Shariah-framed as actual-loss compensation (not riba): months 1–6 6%, 7–12 5%, 13–24 4%, 25–36 3%, 37–48 2%, 49–60 1%, with a 6-month minimum floor (greater of 6% of remaining balance or six months of premium). Sooner’s IRR by termination timing (M3 38.1%, M6 23.4%, M12 16.2%, M24 14.4% worst, M36 15.1%, M48 16.7%, no-prepay 18.7%) confirms the schedule protects the worst-case mid-life prepayment while staying competitive — UAE personal loans charge 1–3% flat; Sooner’s declining schedule is in the same neighbourhood at the back end and only steeper early, where actual loss is largest. Base-case CPR 8%/yr costs only ~−0.5% gross yield. Needs disclosing on the KFS; it is not a gap.

G-f — “RERA rent caps constrain recovery.” Moot. The owner is Sooner’s sub-tenant; the sole eviction trigger is non-payment, the fast Dubai Law 26/2007 Article 25(1) in-term route (30-day notice, corporate-lessor-eligible, no 12-month notarised notice, no personal-use natural-person ground, no 2-year re-letting ban). RERA’s rent-cap index and sitting-occupier protections only matter if Sooner were raising rent on or removing a performing tenant — which the product never does. On default there is no protected sitting tenant; Sooner takes vacant possession and re-leases at market. The concern dissolves on the product mechanics.

G8 — “the 6–11% closing-fee range is a defect.” It is correct, not a defect. The closing stack (DLD 4%, agency 2%, bank arrangement 1%, registration/trustee/valuation/NOC/conveyancing + 5% VAT on services) lands ~6–11% of value depending on whether agency is paid, mortgage size, and VAT-able mix. Sooner finances the actual invoiced number, not a fixed percentage; the % band is an indicative guardrail. Separately, the AED 50,000 absolute floor is wrong: at a 500k property an 8% fee is only 40,000, so the floor would auto-decline in-band facilities on smaller properties. Drop the floor to 30,000.

Gov-h — “are bank statements via Lean enough for BNPL/PDPL?” Necessary but not fully sufficient, and the honest answer says so. Bank-statement parsing reliably catches BNPL/PDPL obligations that settle through the monitored account (Tabby, Tamam, postpay debits). The gap: (1) not all BNPL providers report to AECB, and an obligation paid from a different account or in cash is invisible to a single-account Lean feed; (2) a BNPL line taken after origination is only caught if Lean monitoring is continuous and an alert is wired. Canonical position: bank statements via Lean are the primary detection layer, backstopped by (a) the mandatory AECB pull (G3), (b) borrower self-declaration in Form 1, and (c) a continuous Lean monitor flagging new recurring debits mid-term. Note separately that “PDPL” is a data-protection compliance obligation, not a bank-statement check — it is handled by a consent gate at Lean-connection time plus a retention policy, not by parsing statements.

R-b — “what source for the recourse ranking? We do not have a rental-liquidity API.” Corrected 2026-06-08: Sooner has no PropSearch-grade void / time-to-let feed, but it DOES hold lease-level DLD rental data — every new lease + ~50% of renewals, each with price, duration, unit type, beds, building/sub-community and location. So the ranking is built on near-direct rental data, not sales: (1) PRIMARY DEPTH — the COUNT of new registered leases per (community × unit-type × beds), trailing 12m, normalised to stock; how many leases actually get signed is the cleanest rental-depth signal and it comes straight from DLD, no vendor. (2) ACHIEVABLE RENT — the actual median registered lease price, by unit-type × beds (not an estimate). (3) TURNOVER / VOID proxies — derived from lease duration + re-registration gaps. (4) DLD SALES volume drops to a tie-breaker only (sales liquidity ≠ rental liquidity — that was the original R-b bug). (5) Public reports (DLD Rental Index, ValuStrat VRI, Asteco / CBRE) for the supply overlay. Recovery yield = median registered rent ÷ median sale price, entirely from DLD. A thin dated manual-override register sits on top for the named communities and post-event corrections. The one signal the lease data lacks is OUR OWN realised re-lease void days, so tiers stay committee-overridable and measured void days are layered in as we operate (R-e stages). Caveat: renewals are ~50% under-registered, so the data over-weights tenant turnover vs stable renewals — favourable for measuring re-lease depth, noted for the occupancy read. Buildable today, no vendor dependency.

R-c — “yield sign is wrong.” Corrected 2026-06-08: do NOT penalise yield directly — yield IS positive recovery cash flow, and a genuinely good high-yield community must not be assumed un-re-leasable. The penalty is CONDITIONAL, not monotone. Score achievable net-of-void yield as an inverted-U for the default case, but GATE the top-band markdown on the depth proxy: if a community shows high yield AND deep Ejari registration volume AND no heavy 12m supply pipeline, it is a genuinely good high-yield re-lease market — score recovery at full (90–100), no haircut. Apply the >8.5% → 55 markdown ONLY when high yield co-occurs with WEAK depth or HEAVY supply (the oversupply / distress signature). The penalty is on the distress signature, never on the yield number itself. Coverage (G15) legitimately still uses gross rent (in-house re-lease, no agent fee); only the TIER ranking uses the conditional net-of-void yield. The two uses of “yield” are different by design — document the distinction to pre-empt the apparent contradiction.

R-f — “no void / time-to-lease / rent-level data.” Same root cause as R-b and R-c: the ranking ran on sales-market data because that is what was available, but the recovery mechanism is the rental market. At launch the proxy panel (Ejari registration volume, listing absorption, rent index, supply pipeline) supplies the depth/trend/rent-level signals R-f asks for, so R-f is not a separate workstream — it rides on the R-b proxy source. The void/time-to-lease signals it really wants come from Sooner’s OWN realised re-leases at Stage 2 (3.13); once measured void days are available, feed median void directly into the LGD model — early-default LGD is driven by exactly how many months of void + re-lease the recovery window spans, so this data sharpens the expected-loss number, not just the tier.

Gov-f — “engine contradicts the spec; Sobha Hartland is inconsistent.” Reconcile by making the page the canonical spec. The Sobha inconsistency is concrete: COMMUNITIES carries Hartland Greens=B, Wilton Park=B, The Crest=C, while the ranking exhibit scores one aggregate “Sobha Hartland” row (sales −58.7%) deriving a lower tier. Resolution precedence: the named Section-A row WINS, because it reflects a building-specific re-lease judgment the master-level volume cannot see. So Hartland Greens=B, Wilton Park=B, The Crest=C stand; the single ranking row is a master-level fallback for unnamed buildings only. Split the aggregate row by sub-community where data allows; where missing, floor depth to the conservative 50 rather than carrying the −59% master aggregate onto Wilton Park.

Gov-e — “you cannot test for disparate impact.” The risk is structural: community auto-declines (JVC District 5, excluded enclaves) and employer-tier declines (T4/T5 SME, brokerage, cabin-crew) are not protected characteristics, but in Dubai’s labour market they map almost one-to-one onto nationality. Today Sooner captures no demographic field, so it literally cannot run the four-fifths/adverse-impact-ratio test a funder will demand. “We don’t collect it” is not a defence — the funder inherits the conduct risk. The fix is monitoring-only demographic capture, hard-walled from scoring, plus quarterly AIR testing and adverse-action reason codes so every decline is explainable.


3. Change Specs — Concrete Edits

All scoring/gate specs target apps/underwriting-criteria/index.html (and, post-extraction, underwriting-contract.js). Implement S-a/S-b/S-c/S-d/S-i together — they interlock.

Reconciliation note — superseded by the D1 founder decision (2026-06-08). The earlier cluster debate over the capped-score mechanism (capacityFactor multiplier vs hard min() caps) is moot: BOTH score-capping designs are DROPPED. D1 keeps the score an additive sum-of-parts (J retained), re-weights the key levers (§3.2), and makes the honesty fix a visible reason-coded override (§3.1) plus the existing gates + the D5 weak+weak rule. Thin-file is 70 average-to-good (D3), not 35. Weight rebasing keeps all ten categories summing to 100 (§3.2).

3.1 Honest score — re-weight + visible override, NO cap (S-a/S-b, SIMPLIFIED 2026-06-08)

Founder decision: the multiplicative capacity-factor / hard-score-cap is DROPPED. The score stays an additive sum-of-parts; the honesty fix is re-weighting + exposing the override. Three parts:

1. Re-weight (§3.2). Lift the two key levers — employment stability (C tenure + J employer) 15→24 and re-leasing (H) 10→15 — and rebase to 100. J (employer sector tier) STAYS in the additive pool — the earlier “remove J” instruction is REVERSED (employer capacity is a legitimate weighted signal, per founder; tenure + seniority + employer is a valid sum-of-parts, not a double-count).

2. Expose the override as a published “Decision Rules” layer (founder 2026-06-08). Today decide() returns decline/committee in early branches before it reaches the score’s own total>=70 line, so the published score can read 85 next to a “decline” with nothing connecting them. Fix: formalise those branches as a NAMED, published Decision Rules layer — a sibling to the gates — and surface it the same way the gates are surfaced. Three visible layers:

Layer What it does On fail/fire
Gates G1–G16 hard pass/fail eligibility + affordability minimums decline / insufficient_data
Decision Rules DR1–DRn for gate-passing files, route the final band on multi-factor interactions, each with a published reason code approve / committee / manual / decline + reason
Score creditworthiness gradient, labelled “creditworthiness if the gates pass,” NOT “the decision” shown alongside the band + reason

Initial Decision Rules (each maps to a §3.16 adverse-action code):

const DECISION_RULES = [
  { id:'DR1', when:p=>p.weakEmp && p.weakRecourse,   band:'decline',  rejectType:'soft', reason:'AA10 weak employer + weak recourse' }, // soft on the recourse leg -> location path
  { id:'DR2', when:p=>p.weakEmp && p.strongRecourse, band:p.empTier==='T5'?'manual':'committee', reason:'AA10 recourse rescue of a weak employer' }, // D5
  { id:'DR3', when:p=>p.empTier==='T3',              band:'committee', reason:'AA10 T3 employer ceiling' },              // optional policy ceiling
  { id:'DR4', when:p=>p.anyManualGate,               band:'committee', reason:'AA-gate manual-review gate present' },
  { id:'DR5', when:p=>p.commTier==='needs_verification', band:'manual', reason:'AA09 recourse community unverified' },
];

Every Gate and Decision Rule carries rejectType (§3.9). DR1 is soft on the recourse leg: a weak-employer + weak-recourse file is not a dead-end — moving to a strong-recourse area triggers DR2 (rescue to manual/committee), so the counter-offer is “you would go to manual review in {strong-recourse areas}.” decide() returns { band, score, ruleId, reason }; the worst band across the fired rules + the score band wins, and the fired rule’s reason is published. So the output reads “Score 82 · declined · DR1: weak employer + weak recourse (AA10),” never a silent flip. The true-veto cases (no employment, ineligible recourse community) are caught by the hard gates + DR1; the score does NOT reproduce them, so no multiplicative cap is needed. (Label “Decision Rules” is cosmetic — could be “Routing Rules”; the structure is what matters.)

3. Weights sum to 100 (§3.2), not 105.

Published sensitivity (honest version): “Employer affects a file in three legitimate, distinct ways — tenure (Cat C), seniority + sector (Cat J), and the employment tenure gate — which sum because they are different predictors of staying employed over five years; plus a published decision ceiling. Re-leasing affects it in three — eligibility (G12), coverage (G15), recovery-quality (Cat H). Neither is double-counted; every contribution is named and the override is visible.”

3.2 CATS re-weighted, sum to 100, J RETAINED (S-i / D1, revised 2026-06-08)

New w values (Gabriel 2026-06-08), J kept in the additive pool:

Cat Factor Old New Δ
A Payment Load (DBR incl. Sooner) 20 17.5 −2.5
B Savings Buffer 20 15 −5
C Employment Stability (tenure) 5 10 +5
D Household Income 10 7.5 −2.5
E UAE Residency 10 7.5 −2.5
F Credit Relationship 10 5 −5
G AECB Score 5 5 0
H Re-Leasing Recourse 10 15 +5
I Lifestyle Alignment (LTV) 5 5 0
J Sector Tier (employer) 10 12.5 +2.5
Total 105 100

Stability (C+J) 15→22.5; re-leasing (H) 10→15 — the two key levers, lifted. Affordability (A+B+D) 50→40; credit (F+G) 15→10; residency E 10→7.5; LTV I held at 5. Thin-file path: F 5→0, its weight folds into G (G 5→10) on the 70 average-to-good substitute (§3.11) → thin sum = 100. Remove the activeWeight divisor (total is already out of 100); keep a guard asserting each path sums to 100. Update the “Weights at a glance” table.

UAE Residency (E) = 7.5% — CONFIRMED 2026-06-08, captured as a 3-year step band. Founder rationale (accepted): on a visa-linked 60-month product, flight risk is a first-order concern — a fled borrower is the worst default mode, and 3+ years of UAE residency is a genuine threshold of stickiness (roots, schooling, banking history, more durable visa). So the weight stays at 7.5 (not lowered). The “3+ years is meaningfully different” effect is a THRESHOLD, not linear, so it is encoded in the BAND SHAPE rather than more raw weight (raw weight would double-count rootedness already present in tenure C + credit F/G). Residency band anchors (piecewise-linear per §3.4, with a steep step at 36 months): 12mo→35, 24mo→55, 36mo→85, 60mo→100. Crossing the 3-year mark is where the flight-risk benefit lands.

3.3 Employer + recourse are sum-of-parts, NOT double-counted (S-c/S-d, REFRAMED 2026-06-08)

Correction (founder): the earlier “double-counted” framing was wrong. Tenure (Cat C) + seniority (a Cat J modifier) + employer-tier (Cat J) summing in the score is a legitimate sum-of-parts — each is a distinct predictor of staying employed over five years (a 10-year senior at a strong employer is genuinely more stable than a 1-year junior at the same employer). A factor also appearing in a minimum gate (the tenure floor; G12 recourse eligibility) is also legitimate — a hard floor plus a continuous score, exactly like DBR has both a 50% cap and a graded score. So: - Employer stays in the score (Cat C tenure + Cat J sector, now weighted 9 + 15) AND the tenure gate. Not de-duplicated — each contribution is named. - Recourse stays in the score (Cat H, weighted 15) AND G12 eligibility AND G15 coverage. Three distinct questions, each named. - The ONLY real fix is exposing the override (§3.1.2): publish a reason code wherever a gate or decision-ceiling overrides the score, and relabel the score “creditworthiness if the gates pass.” Publish one honest sensitivity sentence per factor (§3.1). No deletion of the score contributions, no multiplicative cap.

3.4 Band interpolation (S-j)

Replace highIsGood/lowIsGood with piecewise-linear interpolation between published breakpoints. Higher-is-better with anchors [(excellent,100),(good,82),(min,62),(floor,35)]:

const r1 = x => Math.round(x*10)/10;
function highIsGood(v,e,g,m){
  if(v>=e) return 100;
  if(v>=g) return r1(82 + (v-g)/(e-g)*(100-82));
  if(v>=m) return r1(62 + (v-m)/(g-m)*(82-62));
  const fl = m*0.7;                       // publish this explicit floor breakpoint per category
  if(v>=fl) return r1(35 + (v-fl)/(m-fl)*(62-35));
  return 35;
}

Mirror for lowIsGood (floor 20). Round to 1 decimal. Lookup categories H and J are unchanged (discrete tiers). Each band table must publish both endpoints (e.g. “DBR 35% scores 82, DBR 50% scores 62, linear between”). Removes the 20-point cliff at 45.0%/45.1% and the 35–45% dead zone.

3.5 decide() rewrite (S-a/S-b/S-e/R10, S-c, S-d)

function decide(p, gates, total, raw){
  const hardFail = GATES.some(g=>g.type==='hard'&&gates[g.id]==='fail');
  // D5: commission-ONLY income is ineligible at the door (no base salary => decline).
  if (hardFail || p.commTier==='auto_decline' || p.commissionOnly) return 'decline';
  const manual    = Object.keys(gates).filter(k=>gates[k]==='manual_review');
  const weakEmp   = p.empTier==='T4'||p.empTier==='T5';
  const strongRec = p.commTier==='A'||p.commTier==='B';
  const weakRec   = p.commTier==='C'||p.commTier==='needs_verification';
  let band = total>=70 ? 'approve' : total>=55 ? 'committee' : 'decline';

  if (weakEmp){
    // D5: a strong-recourse community RESCUES a weak-employer file to review in ANY DBR band,
    // because every hard gate (incl. G9 DBR<50%) already passed above. NO extra 45% floor.
    if (strongRec)    band = (p.empTier==='T5' ? 'manual' : 'committee');
    else if (weakRec) band = 'decline';
    else              band = 'manual';                              // unknown recourse -> manual
  } else {
    band = worse(band, EMP_MAX_BAND[p.empTier] ?? 'committee');     // T1/T2 approve-capable, T3 committee
  }
  band = worse(band, REC_MAX_BAND[p.commTier] ?? 'manual');         // recourse eligibility ceiling
  if (manual.indexOf('G13')!==-1) band = worse(band,'manual');
  if (manual.length>0)            band = worse(band,'committee');
  return band;
}
function worse(a,b){ return BAND_RANK[a] <= BAND_RANK[b] ? a : b; }

Caller: decide(p, gates, sc.total, sc.raw). Two D5 changes from the earlier draft: (1) the affordOK (DBR≤45% + 6mo) floor is REMOVED — the hard 50% DBR gate is the only affordability line for the recourse rescue; the 60-month stress gates (§3.12) are the backstop. (2) The commission-broker 24-month-statement rescue path is REMOVED entirely — commission-only income is simply ineligible (p.commissionOnly short-circuits to decline). The thin-file standalone committee cap is also dropped (§3.11).

D1 visible-override requirement. Every branch in decide() that returns a band other than the pure score outcome (total>=70?approve:total>=55?committee:decline) MUST attach a published adverse-action reason code (§3.16) and return it alongside the score, e.g. { band:'decline', score:82, reason:'AA10 — T5 employer in a weak-recourse community' }. The displayed score is labelled “creditworthiness if the gates pass,” NOT “the decision,” so an 82-next-to-a-decline is self-explaining rather than a silent flip. No total multiplier / capacity cap is applied to the score (§3.1) — the score is the raw re-weighted sum (§3.2); the override is the visible, reason-coded layer on top.

3.6 Employer recalibration table (S-f — FIXED, drop-in)

Six edits to index.html. Full rows below.

(1) EMPLOYERS array (lines 854–856):

{ name:'ADNOC', aliases:['ADNOC','Abu Dhabi National Oil Company'], tier:'T1', industry:'State energy', macro:{peace:'T1','crisis-iran':'T1'} },   // T2->T1, closes R2
{ name:'SEHA', aliases:['SEHA','Abu Dhabi Health Services','SEHA Hospital'], tier:'T1', industry:'Public healthcare (counter-cyclical)' },     // T2->T1
{ name:'DHA / Dubai Health', aliases:['DHA','Dubai Health Authority','Dubai Health','Latifa Hospital','Rashid Hospital','Dubai Hospital'], tier:'T1', industry:'Public healthcare (counter-cyclical)' },   // NEW
{ name:'Microsoft UAE', aliases:['Microsoft','Microsoft UAE','Microsoft Gulf'], tier:'T2', industry:'Big Tech (UAE-domiciled, local contract)', seniority:[
    { match:'l7|director', tier:'T1', reason:'Big Tech Director (L7+) on a UAE contract resolves to T1-adjacent capacity.' },
    { match:'senior engineer l6|principal', tier:'T2', reason:'Senior engineering at L6+ holds T2.' } ] },
{ name:'FAANG Remote', aliases:['FAANG Remote','Remote FAANG','Meta Remote','Google Remote','Amazon Remote','Apple Remote'], tier:'T3', industry:'Foreign employer, remote worker', note:'Foreign-regional/remote: 2025-26 AI-layoff exposed; no UAE EOSB. Caps at committee.' },
{ name:'Mediclinic / Aster / NMC (clinical)', aliases:['Mediclinic','Aster','Aster DM Healthcare','NMC','NMC Health','Mediclinic Middle East'], tier:'T3', industry:'Private healthcare (clinical staff)', seniority:[
    { match:'consultant|head of department|medical director', tier:'T2', reason:'Senior consultant / medical director resolves to T2.' } ] },
{ name:'Better Homes', aliases:['Better Homes','Betterhomes'], tier:'T5', industry:'Residential real estate brokerage', commissionBroker:true, note:'A commission-ONLY broker is INELIGIBLE (D5) — no base salary => decline at the door. An applicant from a brokerage who ALSO holds a base salary is assessed on that base salary (commission at 50%, §3.7); the recourse rescue (§3.5) then applies normally on the base-salary file.' }

(2) EMP_INDUSTRY array (replace 888–903): State energy T2→T1; Public healthcare T2→T1 (“most counter-cyclical UAE sector”); ADD “Healthcare (private / clinical staff)” T3; SPLIT Big Tech → “Big Tech (UAE-domiciled entity, local contract)” T2 and “Big Tech (foreign-regional / remote / offshore contract)” T3 (“AI-layoff exposed; no UAE severance”). Brokerage/commission/cabin-crew/gig stays T5 with the 24mo precondition note. Self-employed stays manual review (R7).

(3) G_INDUSTRY array (replace 921–935): state_energy T1; public_health T1; ADD {v:'healthcare_clinical', l:'Healthcare (private / clinical)', tier:'T3'}, {v:'bigtech_uae', l:'Big Tech (UAE-domiciled)', tier:'T2'}, {v:'bigtech_remote', l:'Big Tech (foreign / remote)', tier:'T3'}.

(4) EMP_SENIORITY array (add to 904–912): { where:'Big Tech (UAE-domiciled)', match:'Director (L7+), Principal / Senior Engineer (L6+)', tier:'T1/T2' }, { where:'Private healthcare (clinical)', match:'Consultant, Head of Department, Medical Director', tier:'T2' }.

(5) EMP_DOCS T5 row: “Manual underwriting; strong re-leasing recourse required. Commission-ONLY applicants are ineligible (D5) — a base salary is mandatory. An applicant who holds a base salary plus brokerage commission is assessed on the base salary (commission counted at 50%, §3.7); a strong-recourse community can then carry the weak-employer file per §3.5 as long as all hard gates pass.”

(6) NEW EMP_GROWTH array (forward affordability projection — never project growth on volatile income):

const EMP_GROWTH = [
  { tier:'T1', annual:0.03, note:'Public/sovereign payroll: +3%/yr.' },
  { tier:'T2', annual:0.02, note:'Blue-chip / state-adjacent: +2%/yr.' },
  { tier:'T3', annual:0.01, note:'International / professional services: +1%/yr.' },
  { tier:'T4', annual:0.00, note:'Local SME / mainstream private: flat.' },
  { tier:'T5', annual:-0.01, note:'Commission / brokerage / gig: -1%/yr conservative decline.' }
];

J_LOOKUP mapping (T1=100, T2=86, T3=62, T4=0, T5=0) is unchanged — only the employer→tier mapping moves. Update RECON R2 → “resolved: ADNOC ratified to T1.” Whole table flagged needs-ratification, owner=Gabriel.

3.7 Income haircuts (S-h)

// RESOLVED 2026-06-08: rental from a registered Ejari lease = 70% (lease-evidenced, low volatility); all other additional/commission = 50%.
qualifyingIncome = baseSalary + 0.70*ejariRentalIncome + 0.50*additionalIncome + 0.50*commissionIncome;
// D5: commission-ONLY income is ineligible — a base salary is mandatory.
const commissionOnly = baseSalary < MIN_BASE_SALARY && commissionIncome > 0;   // MIN_BASE_SALARY = the G6 base floor
// commissionOnly short-circuits decide() to 'decline' (§3.5).

Use qualifyingIncome (not gross declared) in: Category D bands, gate G6, and the DBR denominator for Category A / G9 / soonerDbr. Reasoning for the haircuts: additional and commission income are volatile and not contractually guaranteed over a 60-month horizon, so half-weighting is the standard lender treatment — it credits the income without letting one good bonus year qualify a file base salary alone cannot carry. Rental income from a registered Ejari lease is lease-evidenced and materially less volatile than commission, so it counts at 70%, not 50% — this reconciles the two cluster variants (50%-flat vs 70%-rental) into one rule. Critical: the G6 household-income FLOOR (≥25,000) must be tested on base salary alone — haircut income cannot lift a borrower over the G6 hard gate, only improve the Category D score above it. Add a base/rental/additional/commission split in the playground so the haircut is visible. Publish: “Base salary counts in full. Rental income from a registered lease counts at 70%. All other additional and commission income counts at 50%.”

3.8 DBR from primitives (G9 / R9)

const soonerMo  = soonerMonthly(p.principal, p.flatRatePct);
const dbrTotal  = (p.existingNonMortgageDebtService + p.bankMortgageInstalment + soonerMo) / p.grossMonthlyIncome * 100;
// assert bankMortgageInstalment is NOT also inside existingNonMortgageDebtService (single-count guard)
r.G9 = (dbrTotal < 50 && p.totalDebt < 60) ? 'pass' : 'fail';

Category A consumes the SAME dbrTotal (single source of truth). Add detected BNPL obligations (Lean parse, Gov-h) as a line in existingNonMortgageDebtService. Resolve R9 ratify→resolved on wiring.

3.9 Hard vs soft reject + path-to-acceptance (G-k / G6 / D7, formalised 2026-06-08)

rejectType is a first-class property of every Gate and Decision Rule'hard' | 'soft' | 'mixed': - hard — failing input is about WHO the applicant is, immutable near-term → dead-end (reapply when it changes). Gates/rules: G1/G11 residency, G2 age, G3/G4 credit (AECB score + defaults), G7 tenure, G16 savings buffer, employer-tier / commission-only, and the ABSOLUTE income floor. - soft — failing input is about WHAT the applicant chose, changeable now → return a counter-offer, never a dead-end. Gates/rules: G8 facility-size/fee% (property-driven), G10 ready-freehold (property choice), G12 community eligibility, G13 rental depth, G15 coverage (property+area), and DR1 weak-employer+weak-recourse (the recourse leg is a CHOICE → location path). - mixed — depends on the cause; the engine runs the test “would a cheaper property or a different area clear this gate, holding the applicant fixed?” → if yes, treat as soft: - G9 DBR: soft on TWO levers — (a) the chosen property drives it → reduce budget; (b) existing non-mortgage debt drives it AND the applicant has the savings to settle that debt → deleverage (settle AED Y of debt, which removes it from the DBR numerator). Guardrail: lever (b) only stands if savings ≥ debtToSettle + requiredBuffer (the 3-month G16 reserve must survive the paydown), else it just trades a DBR fail for a savings-gate fail. HARD only when neither lever works (debt too large to settle from savings AND already at the floor property). - G6 income: soft against the per-deal structural floor (cheaper property → lower required income); hard below the absolute AED 25,000 floor.

Precedence (founder call 2026-06-08): hard dominates. If ANY hard reject fires → hard decline, and the soft counter-offer is SUPPRESSED (do not tell a credit-declined applicant “you’d qualify cheaper” — false hope + a fair-lending hazard). Only a file whose failures are ALL soft gets the path-to-acceptance below. The engine computes rejectClass = anyHard ? 'hard' : (anySoft ? 'soft' : 'pass').

Soft reject → counter-offer. A soft decline returns the controllable levers (the original D7 path, now the soft-reject handler):

3.9a Structural-income fail-fast (G-k / G6)

Evaluated immediately after G6, before scoring:

const requiredIncomeFloor = (p.bankMortgageInstalment + soonerMo) / 0.50;
r.G6 = (p.qualifyingIncome>=25000 && p.qualifyingIncome>=requiredIncomeFloor) ? 'pass' : 'fail';
// fail reason distinguishes 'below 25k backstop' vs 'below structural floor for this facility'

Publish: “The AED 25,000 floor is a backstop; the binding floor is computed per deal as (mortgage + Sooner monthly)/0.50, which is what G9 enforces. The two are reconciled so no borrower passes the income floor only to be declined later on DBR.”

Path-to-acceptance (founder 2026-06-08) — controllable declines return a counter-offer, not a dead-end. Affordability misses driven by a CONTROLLABLE input (the borrower’s property budget) must not be flat-declined. Split decline reasons: - Uncontrollable (residency G1/G11, age G2, AECB defaults G3/G4, employer tier) → hard decline. - Controllable (budget too high for income, i.e. G6/G9 fails only because the chosen property price drives mortgage + Sooner monthly over 50% DBR) → conditional decline WITH A PATH. Back-solve the 50% DBR ceiling to the maximum supportable property price and financed fee, and return a counter-offer instead of “declined”:

// max total monthly debt service at the 50% ceiling, net of existing non-mortgage debt
const maxDebtService   = 0.50*p.grossMonthlyIncome - p.existingNonMortgageDebtService;
// Sooner monthly scales with the financed fee (~8% of price); mortgage instalment scales with the mortgage (price - downpayment).
// Solve maxDebtService = mortgageInstalmentPerAed(price) + soonerMonthlyPerAed(0.08*price) for price:
const maxAffordablePrice = solveMaxPrice(maxDebtService, p);   // monotonic; bisection on price
const maxFinancedFee     = 0.08*maxAffordablePrice;            // indicative; actual = invoiced closing stack
if (controllableFail) { r.decision='conditional'; r.maxAffordablePrice=maxAffordablePrice; r.maxFinancedFee=maxFinancedFee; r.reasonCode='AA04-PATH'; }

// DBR deleveraging lever (founder 2026-06-08): if existing non-mortgage debt is what pushes DBR over 50%,
// and the applicant can settle enough of it from savings while keeping the 3-month buffer, offer that path too.
const requiredBuffer = 3 * (p.bankMortgageInstalment + soonerMo);          // the G16 reserve, post-deal
const dbrOver        = Math.max(0, dbrTotal - 50);                          // pp over the ceiling
const debtToSettle   = (dbrOver/100) * p.grossMonthlyIncome;               // monthly service to remove; map to principal via the debt's factor
if (dbrFailFromExistingDebt && p.savings >= debtToSettle + requiredBuffer) {
  r.decision='conditional'; r.settleDebtToQualify=debtToSettle; r.reasonCode='AA07-PATH';
}

Publish/UX: “You qualify at up to AED X property / AED Y financed fee at your current income (reduce your budget), OR by settling AED Z of existing debt (you have the savings to do so and keep your reserve).” New output fields maxAffordablePrice + maxFinancedFee (budget lever, AA04-PATH) and settleDebtToQualify (deleverage lever, AA07-PATH, only when savings ≥ debtToSettle + requiredBuffer).

Second controllable lever — LOCATION (D7, extended 2026-06-08). A decline can also be driven by the community the borrower chose (an auto-decline community per G12, or a community-tier that fails the recourse rules). That is controllable — the same borrower may qualify in a different area. So when the ONLY failing reason is the community:

// re-run the community-dependent gates (G12 + recourse cap) across the eligible community set, holding the rest of the profile fixed
const eligibleCommunities = COMMUNITIES.filter(c => c.tier!=='auto_decline')
  .filter(c => decideWithCommunity(p, c) !== 'decline')
  .map(c => ({ name:c.master, tier:c.tier }));
if (communityOnlyFail && eligibleCommunities.length) {
  r.decision='conditional'; r.eligibleCommunities=eligibleCommunities; r.reasonCode='AA09-PATH';
}

Publish/UX: “Your profile qualifies — but not in {chosen community}. You would qualify in {eligible areas}.” New output field eligibleCommunities + reason code AA09-PATH. For everything Sooner controls (budget, what they buy, AND where they buy) the default is rejection-with-a-path, never a dead-end no.

3.10 Gate fixes (G2, G3, G8)

3.11 Thin-file substitute = average-to-good (S-g — DECIDED 2026-06-08, REVERSED)

raw.G = thin ? 70 : highIsGood(p.aecb,740,680,550); Substitute 70 (average-to-good), NOT a conservative 35. Founder decision: Sooner is not an unsecured lender — the master-lease property recourse offsets unknown credit history, so a thin file is not the elevated risk it is for an unsecured product. CATS G band label: “thin-file substitute 70 (average-to-good — property recourse offsets absent history).” Keep F at 0% and G at 15% on the thin-file path. No standalone committee cap on thin files (the earlier conservative cap is dropped); the recourse decision ceiling (REC_MAX_BAND, §3.1) already independently caps a thin file that also sits in a weak re-lease community, so a thin file in a thin re-lease market does not get a free pass without double-penalising thin files in strong-recourse communities.

3.12 New affordability gates (F-f / R18)

// NEW hard savings gate
r.G16 = p.savingsMonths >= 3 ? 'pass' : 'fail';                          // auto-decline below 3 months
// NEW stressed-DBR check (route to committee on fail)
const haircut = {T1:0.05,T2:0.05,T3:0.10,T4:0.15,T5:0.15}[p.empTier];
const stressedDbr = dbrTotal / (1 - haircut);
if (stressedDbr >= 55) band = worse(band,'committee');
// NEW thin-affordability-corner waterfall step (before auto-approve)
if (p.qualifyingIncome<=27500 && dbrTotal>=47 && p.savingsMonths<=3) band = worse(band,'committee');  // reason AA13

3.13 Recourse-ranking rubric v2 — on actual DLD rental lease data (R-b/R-c/R-e/R-f)

Data reality (founder 2026-06-08): Sooner holds lease-level DLD rental data — a record of every NEW lease plus ~50% of renewals (renewals are widely under-registered), each with price, duration, unit type, beds, building/sub-community and location. This is near-direct rental data, not a vague proxy and not a paid vendor. The one thing it does NOT contain is OUR OWN realised re-lease void days, so the rubric is staged: lease-data signals now → our measured void days layered in as we operate. Retire the SALES-volume dldVol field. Fields per (community × unit-type × beds), joined by PSL code, refreshed monthly: - DLD rental panel (available today): newLeaseVol12m (count of new registered leases, trailing 12m, normalised to stock — the PRIMARY depth signal: how many leases actually get signed = how deep the re-lease market is), medianRegisteredRent (actual achievable rent, by unit-type × beds), leaseDurationMix + renewalGap (turnover/void proxies derived from lease terms and re-registration gaps), medianSalePrice (for the yield), newSupplyPipeline12m (public reports). dldSalesVol drops to a tie-breaker only. - Measured panel (Stage 2+, layered in as it arrives): voidDays (median days-to-let from our own realised re-leases), timeToFirstOffer. - Caveat to publish: renewals are ~50% under-registered, so the data over-represents tenant TURNOVER vs stable renewals. This is fine — even favourable — for measuring re-lease DEPTH (turnover IS the re-lease market we would tap on default); note it so the occupancy/stability read is not over-stated.

Signals & scoring: 1. DEPTH (≈0.45) — at launch on newLeaseVol12m percentile within unit-type × beds: top-quartile→100; 2nd→82; 3rd→62; bottom→42; null→50 (conservative floor). Once a community has ≥5 realised re-leases, layer in measured voidDays: <30→100; 30–45→85; 45–60→70; 60–90→55; 90–120→42; >120→30. dldSalesVol only breaks ties; it is never the primary depth driver (using sales for rental depth was the original R-b bug). 2. STABILITY (≈0.35) on rent trend + supply: trend >−2%→88; −2 to −5%→72; −5 to −9%→55; <−9%→40. Subtract up to 15pts if newSupplyPipeline12m > 15% of stock. null→50. 3. RECOVERY (≈0.20) = NET-OF-VOID achievable yield = medianRegisteredRent*(1 − voidProxy/365)/medianSalePrice, scored as a CONDITIONAL inverted-U (R-c): <4%→50; 4–5.5%→75; 5.5–7%→100; 7–8.5%→80. High-yield band (>8.5%) is conditional, NOT auto-penalised: if depth>=70 && newSupplyPipeline12m<=15% (genuinely good high-yield, deep, not oversupplied) → score 90 (high yield is real cash flow, do not assume it cannot be re-leased); else (high yield + thin depth or heavy supply = oversupply signature) → 55. The markdown lands on the distress signature, never on the yield number itself.

recourseScore = 0.45*depth + 0.35*stability + 0.20*recovery. deriveTier: ≥79 A; 65–78 B; 50–64 C; <50 needs_verification; hardBlock → auto_decline. Coherence (R-f): the SAME void figure (proxy then measured) feeds recoveryModel.evictionCarryMonths/collectionWindowMonths (replace flat 3/12 with community void) and the correlated-void concentration clusters — one rental-liquidity surface drives ranking + recovery carry + provision + concentration. Manual override register per row: {tier, reason, effectiveDate, expiry, setBy}.

Staged calibration (R-e) — event-triggered on realised re-lease COUNT, not a calendar date (we cannot calibrate on re-leases we have not had): - Stage 0 (now → launch): proxy rubric live; every tier committee-overridable. - Stage 1 (~10 realised re-leases OR 2 quarters, whichever first): sanity-check the Ejari-volume depth proxy vs observed void on the first defaults; override obviously-wrong tiers. - Stage 2 (~30 realised re-leases): swap the volume proxy for MEASURED median void days where ≥5 observations exist; re-baseline stability bands off the post-Iran-shock stabilisation window (not the trough). - Stage 3 (~50 realised re-leases / ~200 originations): full back-test of tier rank-ordering vs realised LGD; promote calibrated cut-offs; run AIR/fair-lending back-test + champion-challenger. - After: quarterly proxy refresh; annual full re-calibration. Until Stage 3 the rubric stays flagged “uncalibrated, committee-overridable.” At current expected phase volumes Stage 3 lands roughly when the book crosses ~200 contracts.

3.14 Void-driven recovery (L6 / F-e — REFRAMED 2026-06-08)

Founder decision: Sooner NEVER evicts a paying party — not a paying tenant, not a paying owner-occupier. Eviction happens ONLY on non-payment. A unit is vacant only when (a) it was bought vacant and is being leased, or (b) a tenant leaves before a replacement is found. So recovery cost is not a function of occupancy type — it is the re-lease VOID (days to re-lease at a competitive price), which Sooner minimises by pricing competitively (the operating incentive is to avoid vacancy). A paying sitting tenant is the BEST recovery case: cash flow continues uninterrupted, zero void.

Drop the earlier investor_tenanted Category-H haircut — it was backwards. Model recovery on void duration instead: - recoveryModel(p) carry = voidMonths (community-measured, from the DLD rental data in §3.13; default to the community’s median void), NOT a flat per-occupancy number. - A unit with a paying occupant at default → Sooner collects/redirects that rent, voidMonths ≈ 0 until the lease naturally ends, then the community void applies. - A non-paying owner-occupier or a vacated unit → the community void applies (time to re-lease at competitive market rent). - propertyUsage is still captured (for ops/reporting) but no longer drives a Cat H haircut; it only informs whether a void applies now (vacant/non-paying) or later (paying occupant).

F-e framing for the recourse section (no new gate): eviction is non-payment-only; the void + the ~2–4mo possession carry are already in recoveryModel and the 1%/yr provision; the owner retains equity and regains control on recovery; competitive re-lease pricing keeps the void short.

3.15 Concentration updates — post-2026-correction (R-h / Gov-g)

Caps as % of outstanding book per phase (P1 = contracts 0–100; P2 = 101–300; P3 = 301+). Daily automated pre-origination check; any breach routes to committee.

Unchanged single-name + borrower-risk caps: single community 50/25/15; single building 20/10/5; single developer 50/40/30; single borrower 10/5/2; self-employed 20/15/10; AECB 550–620 15/10/10; DBR 45–50% 20/15/10. Credit-tier: A(740+) no limit; B(680–739) 40; C(620–679) 20; D(550–619) 10; <550 ineligible.

Cluster changes: | Cluster | Old (P1/P2/P3) | New (P1/P2/P3) | Reason | |—|—|—|—| | Marina + JBR + JLT | 30/25/20 | 22/18/14 | Premium rents −~15%, JLT vol −60%, Marina −40%; rent-based recovery impaired | | Meydan + Sobha Hartland + Creek Harbour (MBR City belt) | 25/20/15 | 18/12/8 | Sobha Hartland sales −59%, supply-heavy, The Crest tier C; largest correlated-supply pocket | | Al Furjan + Discovery Gardens (JVC removed) | 25/20/15 (was JVC+AF+DG) | 20/15/12 | Al Furjan sales −44%; JVC pulled | | Springs + Greens + Mira | — | 30/25/18 (held generous) | Resilient owner-occupier, ValuStrat positive QoQ, low voids; book should lean here |

New sub-limits: JVC District 5 → 0/0/0 (matches G12 auto-decline). Aggregate recourse-tier-C exposure → 12/8/5. Low-yield-community aggregate (recovery band “low”/“low-mid”) → 25/20/15.

Macro overlay: under a declared “crisis” regime (matching the employer-tier crisis-iran switch), ALL community caps tighten by 5 percentage points automatically. Add correlated-default stress to the daily check (flag if >X% of book sits in communities that moved together in the last correction).

Reconciliation rules: every cluster member must exist in COMMUNITIES; cluster allowance ≤ single-community cap; any G12 auto_decline community carries an explicit 0% cluster allowance; the cluster list and ranking must not disagree on direction. Encode as CONCENTRATION object + checkConcentration(book, deal, phase). RECON rows R22 (re-baseline) + Gov-g (as-code), status ratify.

3.16 Fair-lending monitoring pack (Gov-e)

  1. Capture (monitoring-only, hard-walled): nationality, gender, age-band in a separate demographics table, no FK the scorer reads. Engine invariant: scoreProfile/decide input objects contain NONE of these keys — enforced by a unit test asserting their absence. Monitoring-only consent line; never shown on the decision screen.
  2. Adverse-action reason codes on every decline/committee: AA01 residency (G1/G11) · AA02 age (G2) · AA03 AECB/defaults (G3/G4) · AA04 income floor (G6) · AA05 tenure (G7) · AA06 facility-size band (G8) · AA07 DBR (G9) · AA08 property/price (G10) · AA09 community recourse (G12) · AA10 employer+recourse interaction · AA11 re-lease coverage (G15) · AA12 score below floor · AA13 thin-affordability corner. Borrower-facing reason maps from the code (no free text).
  3. Quarterly disparate-impact test: selection rate by nationality-group and gender; AIR = each group’s rate / highest group’s rate, flag AIR<0.80; controlled logistic regression decline ~ demographic + AECB + income + DBR + employer-tier + community-tier to report whether the gap survives credit controls.
  4. Validation / champion-challenger: log every decision with all inputs + outcome + reason codes from day one; at ~200+ originations back-test the score’s rank-ordering vs realised default and re-run AIR on realised declines; promote a challenger only on out-of-sample lift.

Add RECON R21, status ratify. Governance callout on the page: monitoring-only wall, AIR<0.80 trigger, reason codes, quarterly cadence. PDPL (Gov-h): add a PDPL consent gate at Form 2 (Lean-connection / OTP step) + a data-retention policy — a compliance deliverable, not an underwriting check.


4. New Artifacts — Ready to Build

4.1 Expected-Loss Model (resolves C6 / L5 / R11 / R17)

Structure: EL = PD × LGD × EAD.

EAD by default month m: outstanding(m) = principal × (1 − min(1, m/60)). Base principal = 161,490 (8.07% of 2,000,000). OID lifts net capital deployed from 161,490 to ~112,600, equity-at-risk to ~28,440.

PD: annual 4% base; monthly hazard h = 1−(1−0.04)^(1/12) = 0.339%/mo. Tier-adjustable multiplier: T1 0.6×, T2 0.8×, T3 1.0×, T4/T5 or thin-file 1.5×; AECB 550–620 1.4×. Front-load the default-month distribution using the hazard to show the early-default tail.

LGD (recovery model): recovery_window = collectMonths × gross_monthly_rent, collectMonths = 12 − evictionCarry(3) = 9 effective months (in-house re-lease, no agent fee, Art.25(1) eviction). gross_monthly_rent = property_value × achievable_net_of_void_yield / 12. LGD(m) = max(0, 1 − recovery_window/outstanding(m)). Worked at m=9: outstanding ~136,667; mid-yield (6%) 9mo = 90,000 → LGD 34%; high-yield (8%) → 12%; low-yield (5%, soft community) → 45%. Late default (m≥24) LGD → ~0 (outstanding amortised below one year of rent).

Per-recourse-tier EL: Tier A ~0.5–0.8%/yr; Tier B ~1.0–1.5%/yr; Tier C ~2.0–3.0%/yr. This is what risk-based pricing would key off if introduced.

Net-margin bridge (the P2/P3/R17 answer): premium income (10/12.5/15%/yr flat) + OID (21,000 seller + 16,000 bank) − 15% CoF on 80% debt (~12% blended) − opex (~1%/yr) − EL (1.0%/yr net base) = net spread → net IRR 22.4%, ROE 53.4%, NIM 12.15%.

HTML calculator pageapps/el-model/index.html, Sooner-branded static, reusing the exact tokens/CSS from underwriting-criteria/index.html: - Inputs (sliders/selects): property_value (2,000,000); fee_pct_of_value (6–11%, 8.07%); pricing_bundle (10/12.5/15); achievable_yield_band; recourse_tier (A/B/C); annual_PD (4%, tier-adjustable); eviction_carry_months (3); collection_window_months (12); cost_of_funds (15%); debt_share (80%); opex_pct (1%); seller_commission (21,000); bank_referral (16,000); default_month (1–60). - Formulas (JS): mirror the engine’s recoveryModel + effectiveAPR + soonerMonthly. outstanding(m), windowRecovery, LGD(m), EL%/yr = PD × portfolio-LGD, net-margin bridge. - Outputs (right column): decision-style banner “EL 1.0%/yr net | 2.4%/yr gross reserve | net IRR 22.4%”; per-default-month LGD/EL curve (inline SVG, no deps); per-recourse-tier EL table (A/B/C); net-margin waterfall (premium → +OID → −CoF → −opex → −EL → net); effective-APR readout matching the Pricing tab. - Build notes: AED formatting “AED 161,490”, no em dashes, no exclamation marks. Add SKIP_PRE_PR_REVIEW=1 mockup-static-html marker + app.json + README.md per repo rules.

Excel companion — el-model.xlsx (all cells formula-driven off Inputs; build via the xlsx skill / openpyxl): - Inputs — labelled parameter block (named ranges). - Amortisation — 60 rows: month, principal_outstanding, premium_accrued, cumulative_buyer_payment, EAD. - PD-LGD — month, monthly_hazard, gross_rent, window_recovery (9mo), LGD(m), EL_contribution(m); totals row = EL%/yr. - Tier-EL — recourse tier A/B/C × achievable-yield → tail-LGD → EL%/yr. - Margin-bridge — per-bundle (10/12.5/15) columns; rows = premium, +seller 21,000, +bank 16,000, net capital 112,600, −CoF (15%×80%), −opex, −EL (1%), = net profit, net IRR, ROE, NIM. Tie-out cells must reproduce 53.4% ROE / 41.5% gross IRR / 22.4% net IRR / 12.15% NIM / 76,320 5y net profit. - Stress — swap EL to 2.4% gross reserve + yield band to “low/soft”; show stressed net IRR (cross-check the M24-worst 14.4% from the termination-timing table).

4.2 Key Facts Statement (resolves C3 / R15)

Product: Sooner Closing Fee Financing — Shariah-compliant Forward Ijara (master lease + re-lease). Provider: Sooner Real Estate LLC under a UAE property-management licence. Compliance: UAE Federal Decree-Law No. 6 of 2025 cost-of-credit transparency; ijara language, not riba/interest. Format: Sooner-branded one-pager (Manrope, paper/ink palette, yellow accent), AED formatting, no em dashes, no exclamation marks. Produce as .docx for any external/customer surface per the external-document-format rule; an in-app HTML version mirrors the Pricing tab. All AED figures driven by the same constants as the engine (principal, rate, effectiveAPR, soonerMonthly) so the KFS never drifts.

Section 1 — What you are getting. Sooner pays your closing fees up front (DLD 4%, agency 2%, bank arrangement 1%, registration trustee, mortgage registration, valuation, developer NOC, conveyancing + 5% VAT on services), typically 7–10% of value. You do not take a loan; Sooner takes a master lease on your property and re-leases it back to you. Your monthly payment is lease rental, not loan interest.

Section 2 — The numbers (illustrative, AED 2,000,000 base case). | Field | Value | |—|—| | Total amount financed | AED 161,490 (8.07% of value) | | Tenor | 60 months | | Premium (flat on original principal) | 10%/yr (full bundle); up to 15%/yr (CFF only) | | Total premium over term | AED 80,745 (full bundle) | | Total you repay | AED 242,235 | | Total cost of credit | AED 80,745 | | Monthly lease rental | AED 4,037 | | EFFECTIVE PROFIT RATE (APR-equivalent, reducing balance) | ~18.7%/yr (full bundle); up to ~27.7% (CFF only) |

Mandatory disclosure: the flat 10–15%/yr headline is NOT the effective cost. Because you repay in equal instalments while the premium is charged on the original amount, the true reducing-balance rate is roughly 1.85× the flat headline. The effective figure is the one to compare against any other financing.

Section 3 — What you pay for, and what you do not. You pay the lease premium on the financed closing fees only. You do NOT pay extra for Home Discovery or Mortgage Brokerage — Sooner is paid for those by the seller (sale commission) and the bank (referral). That is why bundling more services LOWERS your premium.

Section 4 — Early settlement (Forward Ijara actual-loss compensation, not a penalty). Months 1–6: 6% of remaining balance; 7–12: 5%; 13–24: 4%; 25–36: 3%; 37–48: 2%; 49–60: 1%. Minimum floor: greater of 6% of remaining balance or six months of premium. Context: UAE personal loans typically charge 1–3% flat; Sooner’s schedule declines to 1% in the final year.

Section 5 — What happens if you do not pay. You keep your property, title, and bank mortgage at all times; Sooner’s recourse is the master lease, NOT a charge on your ownership or equity. If you stop paying, Sooner may take possession under Dubai Law 26/2007 Article 25(1) (30-day notice) and re-lease to recover the amount outstanding. Once Sooner has recovered the outstanding balance from re-lease rent, Sooner EXITS the master lease and you regain full possession, with your mortgage and equity intact. You never lose your ownership stake.

Section 6 — Is this worth it (illustrative, not guaranteed). Dubai property has averaged ~+5%/yr, so a 2,000,000 home gains ~100,000/yr; buying earlier through Sooner captures that appreciation and avoids continued rent, generally exceeding the premium over a typical hold. Compare your total cost using the rent-vs-buy calculator: https://sooner-rent-vs-buy.pages.dev/. Property values can fall as well as rise; the Section 2 figures are the guaranteed terms, the value figures here are not.

Section 7 — Your obligations and our monitoring. Affordability is assessed at origination including this monthly rental in your total debt-burden ratio (must stay under 50%). Sooner maintains a continuous Lean Tech (UAE open banking) connection to your nominated account for the life of the facility.

Section 8 — Governing terms. A civil master lease and re-lease under a property-management licence; not a finance-company consumer loan. Full terms in the Master Lease Agreement and Re-Lease Agreement, which prevail over this summary. Cost-of-credit transparency per Federal Decree-Law No. 6 of 2025.

buildKFS(p) computes Sections 2/4/5–7 from the actual financed amount and selected bundle; Sections 5/8 are fixed copy; effective APR from effectiveAPR(ratePct). The KFS must be shown BEFORE any customer commits (regulatory), generated per-deal at offer, and stored.


5. Canonical Page + Forms-Embedding Architecture

Recommendation: one shared contract module, two consumers (canonical page + forms), one server-side authority (engine). No logic fork is possible because all three import the same underwriting-contract.js. This is the structural fix for R-d engine-parity and Gov-f.

5.1 Four-layer restructure of apps/underwriting-criteria/

Layer 1 — underwriting-contract.js (the source of truth; plain ES module, zero DOM, deterministic):

export const CONTRACT = {
  version: '1.0.0', ratifiedAt: null,
  product: { name:'Closing Fee Financing', structure:'Forward Ijara / 5-year master lease',
             tenorMonths:60, premiumFlatPctRange:[10,15], feePctOfValueRange:[6,11] },
  gates: GATES, pdGates: PD_GATES,
  categories: CATS,            // A-J re-weighted to sum 100 (J RETAINED; D1/S-i, §3.2)
  decisionSteps: DECISION_STEPS,
  pricing: PRICING,            // bundle -> rate + effectiveAPR column
  employers: EMPLOYERS, empRules: EMP_RULES, empIndustry: EMP_INDUSTRY, empGrowth: EMP_GROWTH,  // S-f
  communities: COMMUNITIES, ranking: RANKING, rankWeights: RANK_WEIGHTS,                          // Gov-f precedence
  concentration: CONCENTRATION,   // Gov-g / R-h, as code
  el: EL_MODEL,                   // C6 / L5
  haircuts: { additionalIncome:0.5, rentalIncome:0.7, commission:0.5 }   // S-h (reconcile rental, Section 6)
};
export function buildProfile(formState){...}
export function evalGates(p){...}
export function scoreProfile(p){...}          // S-a caps + S-j interpolation
export function decide(p,gates,score,raw){...} // simplified waterfall, S-b
export function recoveryModel(p){...}          // L6 propertyUsage differential
export function expectedLoss(p){...}           // 4.1
export function buildKFS(p){...}               // 4.2
export function checkConcentration(book,deal,phase){...}
export function resolveCommunityTier(building){...}    // named carve-out > data rank > manual (Gov-f)
export const FIELD_MAP = {...};                // promoted from the data-dictionary feeds[] inventory

Layer 2 — index.html renders FROM the module. Replace the ~700 lines of inline consts with import { CONTRACT, scoreProfile, decide, ... } from './underwriting-contract.js'; every tab iterates CONTRACT.*, no duplicated literals. The playground calls the SAME scoreProfile/decide the forms and engine call.

Layer 3 — versioning + ratification ledger. Header version badge (CONTRACT.version), ratifiedAt, git SHA in footer. New “Ratification ledger” tab rendering every RECON item with status==='ratify' as {id, title, recommendation, owner:'Gabriel', ratify/hold toggle}. Signing an item sets CONTRACT.ratifiedAt and flips it to resolved.

Layer 4 — new tabs. “Expected loss” (renders EL_MODEL), “Key Facts Statement” (renders buildKFS(playgroundProfile)), “Concentration” (renders CONCENTRATION by phase + cluster map). Plus a “Download contract JSON” button serializing CONTRACTunderwriting-contract.json so the engine team can diff the deployed config against the canonical truth.

5.2 Field-to-gate map (forms embed)

FIELD_MAP is promoted from the data-dictionary feeds[] inventory (it already exists as data — this makes it the contract), keyed formFieldId → {gates:[], cats:[], step, requiredFor:[]}: - Form 1 (lead.tsx): budget→{G8,G10}; areas→{G12,catH}; residency→{G1,G11,catE}; fixedIncome→{G5,G6,catD}; employmentType→{G14}; propertyType→{catH}. - Form 2 (preapproval.tsx): serviceBundle→{pricing}; income→{G5,G6,catD}; additionalIncome→{catD, haircut 0.5}; employerName→{catJ,G7}; roleSeniority→{catJ}; tenure→{G7,catC}; mortgage/personalLoan/carLoan/cards→{G9,catA}; savings→{catB}; aecbBand→{G3,catG}; propertyUsage→{catI, extend to catH per L6}. - Form 2.5 (co-applicant): mirrors F2 employment + liabilities → joint G6/G9/catD recompute. - Form 2.7 (property): community/building→{G12,catH}; listingPrice→{G8,G10,catA,catI}; ready→{G10}; freehold→{G10}. - Form 3 (documents.tsx): each upload VERIFIES a declared value, flipping collect:'declared'→'verified' and triggering the authoritative engine pass.

5.3 Where each check runs (progressive disclosure — run a gate at the earliest step all its inputs exist)

5.4 Build sequence

  1. Extract underwriting-contract.js from the canonical page (Layer 1). Publish as workspace package @sooner/underwriting-contract. Apply all Section-3 fixes IN the module.
  2. Canonical page imports it and renders from it (proves the contract). CI golden-profile fixture must produce identical decide() output on the page.
  3. Forms import the package (sooner-underwriting-engine/apps/web); wire FIELD_MAP-driven client checks per 5.3; add the Likelihood chip.
  4. Engine imports the package for the authoritative server pass; retire any duplicated engine-side scoring config; reconcile the PSL community-tier DB to the contract’s tier map (Gov-f).
  5. CI parity gate: the SAME golden-profile fixture must produce byte-identical decide() output across (a) canonical playground, (b) form client chip, (c) engine server pass. A diff fails the build. This is the structural guarantee of one source of truth.

Why this order: the contract module ships FIRST so every later consumer imports rather than re-implements. Forms get instant-feedback UX for free (same fns). Engine parity (R-d) becomes structural, not a perpetual reconciliation chore. The canonical page stops being a mockup and becomes the literal code the product runs. Track engine parity as a single Linear ticket so “is it fixed” has one answerable status; until the engine imports the module, add a banner to the Reconciliation tab: “ENGINE PARITY: the live engine still hard-declines T4/T5 and takes commTier as input. This page shows the ratified target. Parity ships when the engine imports underwriting-contract v1.0.”


6. Open Items — Genuinely Need Data / Calibration / Legal / Founder Input

  1. Recourse ranking is render-only (R-d). Still an offline exhibit; the engine takes tier as a set input. Blocked on: ratify the R-b/R-c/R-f rubric → then build resolveRecourseTier(propertyPSL, unitType), wire R10 into the live waterfall, emit the three audit signals. Sequence AFTER rubric ratification.

  2. Rubric calibration (R-e) — “when?” answered. Event-triggered on realised re-lease COUNT, not a calendar date (impossible to back-test without realised outcomes). Stage 1 at ~10 re-leases (or 2 quarters): sanity-check the Ejari-volume depth proxy vs observed void; override obviously-wrong tiers. Stage 2 at ~30 re-leases: swap the proxy for measured median void days (≥5 obs/community); re-baseline bands off the post-Iran-shock stabilisation window (not Feb-2026 pre-shock or the trough). Stage 3 at ~50 re-leases / ~200 originations: regress realised LGD/void on each signal, drop non-discriminating signals, re-weight survivors, set A/B/C cut-offs at the LGD breakpoints, run AIR + champion-challenger. After: quarterly proxy refresh + annual full re-calibration. At current expected phase volumes Stage 3 lands ~when the book crosses 200 contracts. Until Stage 3 the rubric stays “uncalibrated, committee-overridable.” Full schedule in 3.13.

  3. Engine parity (R-d engine). Three queued, none merged: R4 ranking→engine wiring, R10 employer×recourse offset, the deriveTier resolver replacing the PSL lookup. Closes structurally when the engine imports the contract module (Section 5).

  4. Data source (R-b/R-f) — corrected 2026-06-08. No rental-liquidity API is available, so the rubric runs on PROXIES we already hold: DLD Ejari rental-contract registration volume (primary depth), listing absorption (Bayut/PF, optional), DLD sales volume (secondary), DLD Rental Index + ValuStrat/Asteco/CBRE reports (trend/supply), and DLD-derived gross yield (Ejari rent ÷ sale price). Build task: assemble the DLD/Ejari + reports panel per (community × unit-type); no vendor contract blocks launch. The measured void/time-to-lease signals arrive from Sooner’s OWN realised re-leases at Stage 2 and sharpen the EL LGD tail then — they are not a pre-launch dependency.

  5. Founder ratification (the ledger). Needs Gabriel’s sign-off to flip resolved: R2 (✔ ADNOC→T1 decided), R5 (additional-income haircut 50%), R7 (self-employed→manual), R9 (DBR from primitives), R10 (employer×recourse + affordability floor), R15 (KFS/APR actual-loss framing), R16 (recovery data source), R17 (provision — see #8), R18 (60mo affordability gates), R19 (score caps/honesty), R20/R21/R22/R23.

  6. Spec variants — ALL RESOLVED 2026-06-08. (a) Capped-score mechanism: DROPPED (D1). No capacityFactor, no min() cap; the score is an additive sum-of-parts (J retained), re-weighted (§3.2), with a visible reason-coded override (§3.1). (b) Thin-file substitute: 70 average-to-good (D3), not 35 — property recourse offsets unknown credit history. (c) Rental-income haircut: rental from a registered Ejari lease = 70%, all other additional + commission = 50% (D4/§3.7). None open.

  7. Legal — early-settlement framing (R15). Confirm the flat-on-original-principal premium and the declining early-termination schedule against the Forward Ijara actual-loss-compensation / compound-interest rules with counsel before the KFS is customer-facing. Also confirm PDPL obligations on the Lean open-banking connection (explicit consent to pull/store bank data for the facility life, data-minimisation, right-to-erasure on decline) — a separate compliance workstream, not an underwriting check.

  8. The 4% vs 1% reconciliation — CANONICAL FIGURE. They are NOT contradictory: 4% is the gross annual PD (probability of default); 1% is the net annual expected-loss provision after recovery; implied portfolio LGD ≈ 25%. The bridge: EL%/yr = PD × LGD, so 4% PD × ~25% blended LGD ≈ 1.0% net EL/yr. The model’s monthly default reserve (~328.5 ≈ 2.4% of principal/yr) is the GROSS tail reserve, sized to the worst early-default tail (4% PD × ~60% tail-LGD).

    Publish three numbers, stop conflating them:

    • PD = 4%/yr — the input (gross default rate).
    • Gross modelling reserve = 2.4%/yr — conservative tail reserve; use in the stress case.
    • Net expected-loss provision = 1.0%/yr — blended-recovery expected loss; use this in the margin bridge and pricing-adequacy test. DEFAULT_PROVISION_PCT = 1 in code is the NET figure.

    Cross-check: 5-year EL ≈ cumulative-PD(18%) × LGD(25%) × weighted-EAD(~80,745) ≈ AED 3,633/deal ≈ 1%/yr of book over 5y — the two methods reconcile, validating the 1%/yr provision, and ~3,633 EL is comfortably covered by the 12.15% NIM. R17 status: validated, not owed.

Files to create: apps/underwriting-criteria/underwriting-contract.js; apps/el-model/{index.html, app.json, README.md}; el-model.xlsx; the KFS .docx (external) + in-app HTML tab; @sooner/underwriting-contract workspace package. Files to edit: apps/underwriting-criteria/index.html (all Section-3 specs); sooner-underwriting-engine/apps/web/pages/apply/{lead,preapproval,documents}.tsx (FIELD_MAP client checks + Likelihood chip); engine server scorer (import the contract module).