Lending-Committee Credit-Risk Skill (task_group_011)
Executable experience for credit-risk / lending-committee committee packets built from the
shared credit-office public REST API. A solver sees only a task prompt + an
answer_template.json + this file + the live API. The tasks in this family always ask you to
pull branch / loan / application / policy / benchmark data, apply a small set of deterministic
credit rules, and emit a JSON object whose shape is fixed by the template.
This is a method document, not a per-task answer key. Read the section map, apply the rules,
and let the template's enums/ordering/precision drive the output.
0. Environment SOP (READ FIRST)
- Base URL:
<remote-env-url> — all endpoints under /api/, JSON, no auth.
- GET only. The environment is read-only for you. Never POST/PUT. Never call
/api/judge
(no test-time judge is available; it is out of scope and not part of the public surface).
- Always
GET /api/health once to confirm the service is up. Use curl -s … | jq to shape JSON.
- Quote any URL containing
?/& in zsh (e.g. curl -s "http://…/loans?min_current_rating=3"),
otherwise the shell tries to glob the ? and the call returns nothing.
branch_id values are uppercase (REDWOOD, LAKEVIEW, SUMMIT, HARBOR, … (uppercase; a task names its own target branch)).
Segment ids look like CIVIC_NC_FIRE_EMS.
Endpoint inventory and what each feeds
| Endpoint |
What it returns |
Feeds answer section |
GET /api/manifest |
policy_version, benchmark versions, seed |
benchmark_version, cross-checks |
GET /api/policies |
the single source of all rules (capacity/concentration, risk-rating tables, CDFI factor scores, CRE weighted-score weights, stress formulas) |
every rule below |
GET /api/branches |
all branches; ?institution_type=bank|credit_union |
branch discovery |
GET /api/branches/{branch_id} |
lending_capacity_q1, sector_ceiling_pct, cre_policy_limit_pct, total_assets, state_code, institution_type, fdic_benchmark_set |
capacity, concentration ceilings, CRE limit |
GET /api/branches/{branch_id}/metrics |
list by quarter; latest quarter row has total_loans_outstanding, nonperforming_loans, delinquency_30_plus_pct, allowance_for_loan_losses, net_charge_offs, total_deposits |
NPA ratio, FDIC variance, concentration denominators |
GET /api/branches/{branch_id}/loans |
loans; filters ?loan_type=, ?payment_status=, ?min_current_rating= |
regrade population, watch-list, CRE exposure |
GET /api/branches/{branch_id}/sector-exposures |
per-sector current_exposure, limit_pct, grandfathered |
sector concentration, flags |
GET /api/branches/{branch_id}/applications |
pending applications; ?loan_type= |
allocation decisions, CRE comparison |
GET /api/benchmarks/fdic/q4-2024 |
FDIC ratios (total_loans_noncurrent_pct, total_real_estate_noncurrent_pct, total_real_estate_30_89_pct, construction_development_*) |
FDIC benchmark variance |
GET /api/benchmarks/ncua/q1-2025 |
rows per state + a US aggregate; ?state_code= filters |
credit-union segment state metrics, peer comparison |
GET /api/credit-union-segments/{segment_id} |
state_code, peer_states, quarterly_capacity, current_outstanding, minimum_checklist, risk_tolerance, internal_context, notes |
segment posture page |
Query-param semantics: ?min_current_rating=N returns loans with current_rating >= N
(ratings are worse-as-they-grow: 1 best … 8 worst). ?loan_type=CRE and ?payment_status=
are exact-match filters. ?state_code=NC returns a single-row rows array.
Metric-minding rules
metrics is a list keyed by quarter (e.g. 2025Q1, 2024Q4). Always pick the
latest (highest) quarter for the as-of date in the prompt.
sector-exposures may carry a per-sector limit_pct override (e.g. Healthcare 0.19 where the
branch default sector_ceiling_pct is 0.21). Use the per-row limit_pct, falling back to
branch.sector_ceiling_pct for sectors not listed.
loans carry nullable factors: dscr, ltv, fico, debt_to_asset, liquidity_months,
collateral_value can all be null. Handle nulls explicitly (see §3).
1. Numeric & ordering conventions (apply everywhere)
- Currency: 2 decimals (USD), round half-up.
- Ratios / percentages / concentrations: 4 decimals.
- Basis points (variance_bps, delinquency_bps): 2 decimals … except NCUA
state_metrics values which are reported as bare integers exactly as the benchmark table
gives them (see §6).
variance_bps is computed from the UNROUNDED variance_ratio, then rounded to 2dp. Do not
compute bps from the already-4dp-rounded ratio (that loses precision — e.g. NPA 1037.49 vs the
1037.0 you would get from the rounded 0.1037). Pattern:
variance_ratio = branch_ratio - benchmark_ratio (unrounded);
variance_bps = variance_ratio * 10000 (round 2dp);
then publish variance_ratio rounded 4dp.
signed variance: branch ratio minus benchmark ratio. A branch worse than benchmark yields
a positive variance_bps (more delinquency / more NPA = bad). Keep the sign.
- List ordering is always explicit in the template — read it. Common orderings: ascending
loan_id; ascending final_rating; ascending action/sector/payment_status (lexicographic
on the enum string); ascending trigger_id; ascending current_rating then payment_status;
descending exposure then ascending loan_id (workout queue); ascending application_id;
ascending alphabetic for reason-code lists.
- All
loan_ids arrays are sorted ascending loan_id (string sort).
2. Risk-rating re-derivation (regrade) — /api/policies → risk_rating
The regrade target population is loans with current_rating >= target_current_rating_min
(default min = 3, i.e. "rated 3 or worse"). Pull them with
GET /branches/{id}/loans?min_current_rating=3.
Factor → rating tables (from policy risk_rating)
Apply each available factor; null factors are skipped (they do not contribute a rating).
- DSCR (
dscr): >=1.5→3, >=1.25→4, >=1.05→5, >=1.0→6, <1.0→7.
- LTV (
ltv): <=0.65→3, <=0.75→4, <=0.85→5, <=1.0→6, >1.0→7.
- Delinquency floor (
payment_status → minimum rating, the "severe-delinquency override"):
Current→null, 30 Days Past Due→4, 60 Days Past Due→5, 90+ Days Past Due→7,
Nonaccrual→8.
Dominant-factor (worst-notch) rule
final_rating = max(available factor ratings) — the worst (highest numeric) rating
produced by DSCR, LTV, and the delinquency floor wins.
- The delinquency floor acts as a hard floor: a
Nonaccrual loan is 8 no matter how strong
DSCR/LTV are; a 90+ Days Past Due loan is ≥7.
- If all factor-derived ratings are null/absent (e.g. an unsecured consumer loan with no DSCR,
no LTV, and
Current), retain the current_rating (no re-derivation possible). You can
only worsen or hold — never improve — during a regrade.
material_downgrade_notches = 2 (policy). A loan whose final_rating - current_rating >= 2
is a material downgrade.
Regrade outputs (per template, e.g. train_001 shape)
target_current_rating_min, target_loan_count, target_exposure = sum of
outstanding_balance over the target population.
final_rating_exposure_totals: group all target loans by final_rating; one row per
final_rating present, ordered ascending final_rating, with loan_count and exposure.
migration_from_current_rating_3: among target loans whose current_rating == 3 (the
threshold entry point), group those that actually moved (final_rating > 3) by final_rating,
with loan_ids. Loans that stayed at 3 are shown in the totals but omitted from this
migration list (it is a migration — only movers).
material_downgrades: every target loan with downgrade_notches >= 2, fields
{loan_id, current_rating, final_rating, downgrade_notches, exposure}, ordered ascending
loan_id. (downgrade_notches = final - current.)
top_problem_credit: the single loan with the worst final_rating (ties → largest
exposure, then lowest loan_id). Include borrower_name, payment_status, and a
recommended_action per §10.
Watch-list action coverage (regrade follow-up)
Subset of regraded loans needing follow-up = those with final_rating >= 6. Group by
recommended_action (§10 mapping). covered_loan_count/covered_exposure = totals over that
subset; by_action ordered ascending by action enum string, loan_ids ascending.
Regrade vs watch-list exclusion: the regrade population is current_rating >= 3 and
uses final_rating for action mapping; the watch-list packet (§9) uses
current_rating >= 6 (adverse) with no regrade (actions keyed off current_rating). Don't
mix the two rating bases.
3. NPA / FDIC benchmark variance
NPA review (train_001-style) uses benchmark metric total_loans_noncurrent_pct (the template
also allows total_real_estate_noncurrent_pct, construction_development_noncurrent_pct).
benchmark_version = "fdic_q4_2024" # from branch.fdic_benchmark_set / /api/manifest
benchmark_metric = one of the template's allowed values (context: NPA → total_loans_noncurrent_pct)
branch_npa_exposure = metrics[latest].nonperforming_loans
branch_total_loans = metrics[latest].total_loans_outstanding
branch_npa_ratio = branch_npa_exposure / branch_total_loans # 4dp
fdic_benchmark_ratio = FDIC[benchmark_metric] # 4dp
variance_ratio = branch_npa_ratio - fdic_benchmark_ratio # 4dp (unrounded for bps)
variance_bps = variance_ratio * 10000 # 2dp, from unrounded ratio
Metric → branch-ratio source mapping (pick by benchmark_metric)
*_noncurrent_pct metrics → branch ratio = nonperforming_loans / total_loans_outstanding.
*_30_89_pct metrics → branch ratio = metrics[latest].delinquency_30_plus_pct (use the field
directly, already a ratio).
A CRE-concentration packet (train_005-style) instead pairs
fdic_benchmark_metric = total_real_estate_30_89_pct with
branch_delinquency_ratio = metrics[latest].delinquency_30_plus_pct. Same variance math; the
branch ratio field name in that template is branch_delinquency_ratio / fdic_variance_*.
4. Lending capacity & allocation (train_002-style)
For a branch's pending-applications allocation packet:
lending_capacity_q1 = branch.lending_capacity_q1 (from /branches/{id})
gross_approved_amount = sum of approved_amount over APPROVED + CONDITIONAL_APPROVE apps
committed_capacity_amount = sum of bank_capacity_used over those same apps
remaining_capacity = lending_capacity_q1 - committed_capacity_amount
Decision enum (approve | conditional_approve | decline | defer | participation_required)
Conditions enum: participation_required | reduced_amount | board_exception | sba_guaranty_required | startup_monitoring | none.
Per-application decision logic (apply in priority order):
- Hard decline reasons first (§8): if any reason code like
low_fico, recent_bankruptcy,
underwater_collateral, policy_floor_missing, documentation_gap fires → decline.
- Credit weakness decline:
high_ltv AND weak_dscr together, or high_ltv AND
startup_risk, generally → decline. A single borderline weakness with strong offsets
(e.g. high FICO) may still approve.
- Capacity: after higher-priority apps are funded, if remaining capacity can't absorb the
app and no other weakness exists →
decline with reason capacity_limit.
- Sector/CRE concentration approach (post_approval pct near or over the sector
limit_pct)
→ do not decline outright; mitigate via conditional_approve with condition
participation_required (sell the excess to participants) or reduced_amount
(approve a smaller amount) or board_exception (committee override).
- SBA-guaranteed apps (
sba_guaranty_pct present) → conditional_approve with
sba_guaranty_required; add startup_monitoring when years_in_business is short (< ~2y).
- Otherwise →
approve (condition none).
approved_amount vs bank_capacity_used
- plain
approve: approved_amount = requested_amount; bank_capacity_used = approved_amount.
sba_guaranty_required: approved_amount = requested_amount;
bank_capacity_used = approved_amount * (1 - sba_guaranty_pct) (the SBA-guaranteed share is
not bank risk/capacity).
participation_required: approved_amount = requested_amount (the full loan is
originated); bank_capacity_used = approved_amount − participated_portion (the sold portion
relieves bank capacity). The participated portion is the amount needed to bring the relevant
sector/large-exposure back under its ceiling. When LTV>1 / underwater, the bank may also retain
only a capped share.
reduced_amount: approved_amount = min(requested, capacity/headroom); bank_capacity_used = approved_amount.
decline/defer: approved_amount = 0.0, bank_capacity_used = 0.0, conditions = ["none"].
priority_ranking
Ordered list of application_id, highest priority first, approved + conditional_approve only
(declines/defers excluded). Priority is committee priority — strategic relationship/quality first,
then core approves, then conditional — not simply by amount. Reverse it for tie-breaks if the
template asks ascending.
5. Concentration (sector ceilings & CRE limit)
Sector concentration
- Per-sector
limit_pct comes from /sector-exposures; default to branch.sector_ceiling_pct
for sectors absent from that table.
grandfathered=1 sectors may sit over ceiling but new approvals may not worsen them without
mitigation (policy capacity_concentration.grandfathering_note).
- Allowed mitigations:
participation_required, reduced_amount, board_exception.
Post-approval concentration denominator (IMPORTANT — do not use total_assets)
- "existing" concentration (e.g. CRE):
exposure / total_loans_outstanding.
- "post_approval" concentration (sector view after funding approved apps):
exposure_after_approval = existing_sector_exposure + approved_amount(s) in that sector;
post_approval_pct = exposure_after_approval / (total_loans_outstanding + gross_approved_amount).
i.e. the post-approval loan book = current book plus the gross new approvals.
- For a single selected CRE app (train_005):
selected_post_approval_cre_concentration = (existing_cre_exposure + selected.requested_amount) / (total_loans_outstanding + selected.requested_amount) — the full requested amount is added (participation is a
decision/condition, not a concentration reducer; the bank still originates the full loan).
over_limit = post_approval_pct > limit_pct (strict >; a value exactly at the limit is not
over).
existing_cre_exposure derivation
Sum of outstanding_balance over GET /branches/{id}/loans?loan_type=CRE. (This is the
loan_type=CRE aggregation, not the sector-exposures table — the two differ for grandfathered/
mixed sectors.)
concentration_flags (per approved app touching a sector)
One row per approved/conditional app whose sector post-approval pct approaches or exceeds the
sector limit_pct (within a thin headroom, e.g. ~0.01, or over). Fields: sector,
application_id, limit_pct, post_approval_pct (4dp), flag (bool), handling (enum:
approve | conditional_approve | decline | participation_required | none). Sort by sector then
application_id.
CRE policy variance (train_005)
cre_policy_limit_pct = branch.cre_policy_limit_pct
existing_cre_exposure = sum(CRE loan balances)
existing_cre_concentration = existing_cre_exposure / total_loans_outstanding # 4dp
selected_post_approval_cre_concentration = (existing + requested) / (total_loans + requested) # 4dp
selected_policy_variance_bps = (selected_post - cre_policy_limit_pct) * 10000 # 2dp, unrounded
6. Credit-union segment posture (train_003-style)
Inputs: GET /credit-union-segments/{segment_id} + GET /api/benchmarks/ncua/q1-2025 (+ optional
?state_code=).
state_metrics
Take the segment's state_code row from the NCUA benchmark. Report integer values exactly as
in the table (no rounding, no /100): delinquency_bps, loan_to_share_pct, roaa_bps,
positive_net_income_pct. benchmark_version = "ncua_q1_2025". (Contrast: ratio fields elsewhere
are 4dp — these NCUA fields are integers.)
peer_comparison
peer_states = segment.peer_states (use verbatim, then sort ascending state code).
nc_vs_us and nc_vs_peer_median: for each of the four metrics, direction is
"higher" | "lower" | "equal" comparing the segment state's value to (a) the US aggregate row
and (b) the median of the peer_states rows.
- Note: for
delinquency_bps higher = worse; for roaa_bps/positive_net_income_pct higher =
better; for loan_to_share_pct higher = more leveraged. The direction is a pure numeric
comparison (higher/lower), the interpretation is separate. Compute medians with the usual
middle-of-three for the 3 peer rows.
posture (enum: continue_approving | continue_with_tighter_conditions | temporarily_pause)
Decision matrix driven by segment.notes, risk_tolerance, internal_context, and the
direction matrix:
- Capacity available and external risk no worse than national/peers →
continue_approving.
- Capacity available but external risk weaker (NC higher delinquency, lower ROAA, lower
positive-net-income vs both US and peer median) →
continue_with_tighter_conditions (add
operating controls, don't pause).
- Capacity exhausted or external risk severely adverse / control breakdown →
temporarily_pause.
controls
required_checklist_gates = segment.minimum_checklist (verbatim set from the endpoint), drawn
from enum: board_authorization, equipment_invoice, fleet_replacement_plan, payer_contract_summary, public_contract_or_tax_support, proof_of_insurance, ucc_or_title_lien.
added_operating_controls derive from internal_context:
- insurance/lien control issue →
pre_close_insurance_binder_verification,
lien_perfection_prior_to_funding;
- staffing/senior-underwriter constraint →
senior_underwriter_second_review;
- recent delinquency watch →
monthly_segment_delinquency_watch;
- external state monitoring →
quarterly_state_benchmark_monitoring;
- capacity overrun possibility →
committee_exception_for_capacity_overrun.
Enum: pre_close_insurance_binder_verification, lien_perfection_prior_to_funding, senior_underwriter_second_review, quarterly_state_benchmark_monitoring, monthly_segment_delinquency_watch, committee_exception_for_capacity_overrun. Sort the set.
escalation_triggers
List of {trigger_id, condition, owner}, ordered ascending trigger_id (ET001, ET002, …).
Select conditions relevant to the segment's risk profile from: segment_recent_delinquency_ge_ 90_bps, missing_insurance_or_lien_exception, quarterly_capacity_exceeded_or_exception_requested, state_delinquency_gap_widens_25_bps. Owner mapping:
segment_recent_delinquency_ge_90_bps → credit_risk_manager
missing_insurance_or_lien_exception → operations_control_manager
quarterly_capacity_exceeded_or_exception_requested → lending_committee_chair
(Include a trigger as a standing escalation gate for the segment even if the current value is just
below its threshold — they are armed gates, not only currently-breached ones. Drop a trigger only
when the segment's profile makes it irrelevant — e.g. omit state_delinquency_gap_widens_25_bps
when the state gap is already the dominant, not a widening-delta, risk.)
interpretation
capacity_status: capacity_available | capacity_constrained | no_capacity — based on
quarterly_capacity headroom vs current origination flow (capacity_available when the quarterly
budget is not exhausted; do not compare current_outstanding stock against quarterly_ capacity flow).
external_risk_status: stronger_than_national_and_peers | mixed_vs_national_and_peers | weaker_than_national_and_peers — from the nc_vs_us / nc_vs_peer_median direction matrix.
risk_tolerance: take segment.risk_tolerance (restrained | moderate | expansive).
committee_message: pick from capacity_available_but_external_risk_weaker | pause_until_state_metrics_recover | routine_approval_path_supported to match the posture.
7. CDFI-style risk classes & factor scoring (watch-list, train_004-style)
From policy.cdfi_factor_scores: score each available factor (null → skip), sum the scores,
map the sum to a class. Null factors are skipped, not zeroed.
Factor score tables
| Factor |
<0.40/>720/>12 |
0.40-0.60/680-720/6-12 |
0.60-0.80/580-679/3-6 |
>0.80/<580/<3 |
ltv |
0 |
2 |
4 |
6 |
debt_to_asset |
0 |
2 |
4 |
6 |
fico (>720→0, 680-720→1, 580-679→3, <580→5) |
0 |
1 |
3 |
5 |
liquidity_months (>12→0, 6-12→1, 3-6→3, <3→5) |
0 |
1 |
3 |
5 |
(LTV/DTA bands are value ranges; FICO/liquidity are their own scales — use the table literally.)
Class mapping (policy cdfi_factor_scores.classes)
| factor_score sum |
risk_class |
| 0-5 |
Prime |
| 6-9 |
Desirable |
| 10-13 |
Satisfactory |
| 14-18 |
Watch |
| >=19 |
Doubtful |
>=14 AND ltv > 1.0 |
Projected Loss (underwater-collateral override) |
The policy text states Projected Loss = ">=19 and ltv>1.0", but the applied rule
escalates any Watch-or-worse score (>=14) with ltv > 1.0 (underwater collateral) to
Projected Loss. Apply this override. (ltv>1.0 is the "underwater collateral" trigger.)
watch_list_summary.risk_classes: list of {loan_id, risk_class, factor_score} for the adverse
population, ordered ascending loan_id. monitoring_cadence = monthly for an adverse
watch-list (rating >= 6); quarterly/semiannual only for healthier populations.
8. Decline reason codes (train_002/005) — derive from application/loan factors
Reason-code enum: capacity_limit, sector_breach, weak_dscr, high_ltv, low_fico, recent_bankruptcy, startup_risk, underwater_collateral, policy_floor_missing, documentation_gap, fdic_adverse_variance, ncua_peer_weakness.
Trigger thresholds (apply to application fields):
high_ltv — ltv > 0.80 (for business/CRE; consumer may tolerate slightly more with strong FICO).
weak_dscr — dscr < 1.25 (below the "pass" pass rating 4 threshold). dscr is None on a
product that doesn't use DSCR (consumer/mortgage) is not weak_dscr.
low_fico — fico < 580.
recent_bankruptcy — bankruptcy_months_ago present and < ~24 months.
startup_risk (decline) vs startup_monitoring (condition): years_in_business < ~2 is
startup; pair with strong credit → startup_monitoring condition on a conditional approve; pair
with another weakness → startup_risk decline reason.
underwater_collateral — ltv > 1.0 (collateral value below loan).
capacity_limit — the only reason when an otherwise-acceptable app is declined because Q1
capacity is consumed by higher-priority apps.
sector_breach — approving would push the sector (or CRE aggregate) over its limit_pct.
fdic_adverse_variance — branch's FDIC benchmark variance is adverse (positive bps, branch
worse than benchmark) for the relevant metric; an ambient reason code on CRE/NPA-sensitive apps.
ncua_peer_weakness — credit-union segment where the state is weaker than national/peers.
policy_floor_missing / documentation_gap — missing documentation_complete or required
checklist gate.
decline_reasons maps each declined application_id → sorted (ascending alphabetic) list of
reason codes. For "competing credit" tasks (train_005), unselected_reason_codes is restricted to
sector_breach | weak_dscr | high_ltv | fdic_adverse_variance. The applications_compared[].reason_ codes on a defer decision mirrors the same list. Always sort reason-code lists ascending
alphabetically.
9. Watch-list stress & workout (train_004-style)
Adverse population = current_rating >= 6 (parameter adverse_rating_min, default 6). Pull via
GET /branches/{id}/loans?min_current_rating=6. No regrade here — use current_rating directly.
+200bp DSCR stress (policy stress)
shock_label = "+200bp"
formula = stressed_dscr = dscr / (1 + 0.18) # watch_list_formula
breach_threshold = 1.0 # policy stress.coverage_breach_threshold
stressed_dscr = dscr / 1.18 # 2dp
breaches_threshold = stressed_dscr < 1.0 # strict <
stress_results.results lists only loans with dscr available (dscr is not None), ordered
ascending loan_id. breach_loan_ids = those with breaches_threshold true, ascending loan_id.
CRE dual stress (train_005-style) — when comparing CRE applications
formula = "dscr * 0.85 / 1.18" # policy cre_dual_stress_formula
(= dscr * 0.85 / (1 + 0.18))
coverage_breach_threshold = 1.0
stressed_dscr = dscr * 0.85 / 1.18 # 2dp
breaches_threshold = stressed_dscr < 1.0
stress.results ordered ascending application_id.
workout_queue
All adverse loans, each {loan_id, exposure, risk_class, payment_status, recommended_action, projected_loss}. Order descending exposure, then ascending loan_id.
exposure = outstanding_balance.
risk_class from §7.
recommended_action from §10 (keyed off current_rating in watch-list packets).
projected_loss = true iff risk_class == "Projected Loss", else false.
severe_bucket_counts
Group the adverse population by (current_rating, payment_status); one row per non-empty bucket.
Fields {current_rating, payment_status, loan_count, exposure}. Order ascending
current_rating, then payment_status (lexicographic on the enum string — "90+ Days Past Due"
sorts before "Current" because '9'(0x39) < 'C'(0x43)).
10. recommended_action mapping (the action enum)
Enum: monitor | watchlist | special-assets | workout | partial_chargeoff_review | legal_referral.
The primary mapping is by the governing rating (which is final_rating after a regrade, or
current_rating for a pure watch-list packet) combined with payment status:
| governing rating |
payment_status |
recommended_action |
| <= 5 |
(any) |
monitor |
| 6 |
(any) |
watchlist |
| 7 |
(any) |
special-assets |
| 8 |
Nonaccrual |
partial_chargeoff_review |
| 8 |
90+ Days Past Due |
partial_chargeoff_review (or workout) |
(workout and legal_referral apply to active-restructuring / fraud-litigation situations; in
the train data the 6/7/8 mapping above covers standard adverse credits. legal_referral is
reserved for fraud/litigation indicators; workout for loans already in active restructuring.)
top_problem_credit.recommended_action and each by_action bucket use this enum, sorted ascending
by action string in by_action.
11. Competing-CRE decision (train_005-style) synthesis
Given two competing CRE applications at one branch:
- Compute each app's weighted CDFI score (policy
cre_weighted_score):
- Weights:
capacity 0.45, collateral_exposure 0.36, conditions 0.11, character 0.05, capital 0.03 (capacity + collateral dominate at 0.81 combined).
- Score each of the 5 Cs 1-5 from application attributes (capacity
DSCR,
collateralLTV, capitaldebt_to_asset = total_debt/total_assets, characterfico/
co_guarantor_strength/years_in_business/prior_delinquencies, conditions~proposed_rate/
sector/documentation/purpose). Lower = better.
weighted_cdfi_score = Σ weight_c × score_c (1 decimal).
score_class: <=2.0 → approve_quality, <=3.0 → conditional, >3.0 → weak.
- CRE dual stress both apps (§9). Flag
weak_dscr reason if stressed_dscr < 1.0.
- Reason codes per app:
fdic_adverse_variance (branch FDIC variance adverse — ambient on
both), sector_breach if the app's sector/CRE would breach, weak_dscr if base or stressed
DSCR weak, high_ltv if ltv > 0.80.
- Decision per app:
approve | conditional_approve | decline | defer | participation_required.
approve_quality score + no breach + stress passes → approve.
conditional score but CRE concentration breaches → participation_required (sell excess;
keep the better credit).
weak score + stress breach → defer (collect more info / sponsor support), or decline
if a hard reason (low_fico/recent_bankruptcy/underwater) fires.
- recommended_path:
selected_application_id = the stronger credit (lower weighted score,
better stress, fewer reason codes). path = the selected app's decision enum.
unselected_application_id, unselected_disposition ∈ {decline, defer} (defer when the
credit is merely weak/not-yet-declinable; decline only on a hard reason). unselected_reason_ codes (restricted enum §8) sorted ascending alphabetic.
- concentration block per §5.
fdic_* fields use total_real_estate_30_89_pct +
delinquency_30_plus_pct.
- conditions set from:
bank_retained_exposure_cap, committee_cre_exception, updated_appraisal_before_close, tenant_roll_and_lease_review, minimum_dscr_covenant_1_25, quarterly_financial_reporting, no_additional_cre_without_committee_review. Choose the set
matching the selected path (participation → bank_retained_exposure_cap; existing CRE already
over policy limit → committee_cre_exception + no_additional_cre_without_committee_review;
CRE deal → updated_appraisal_before_close + tenant_roll_and_lease_review +
minimum_dscr_covenant_1_25 + quarterly_financial_reporting). Sort ascending alphabetic.
12. Common misjudgments & exclusion rules (guardrails)
- Regrade population vs watch-list population are different. Regrade =
current_rating >= 3
using final_rating for actions. Watch-list = current_rating >= 6 using current_rating
for actions. Do not apply final_rating to a watch-list packet or current_rating to a regrade's
action mapping.
- Severe-delinquency override:
Nonaccrual → rating 8 and 90+ Days Past Due → rating ≥7 are
floors, not suggestions. A Nonaccrual loan never gets a final_rating below 8 even with
DSCR 2.0 and LTV 0.4.
- Null factors are skipped, never zeroed (regrade dominant-factor, CDFI factor_score). A loan
with
dscr=None, ltv=None, payment_status=Current keeps its current_rating.
- migration_from_current_rating_3 lists only movers (final_rating > 3) among loans whose
current_rating == 3. Loans that stayed at 3 appear in
final_rating_exposure_totals but not in
the migration list.
- material_downgrades uses
>= 2 notches (policy material_downgrade_notches), across the
entire regrade target population (not just current_rating==3 loans).
- Concentration denominator is the loan book, never
total_assets. Existing →
total_loans_outstanding; post-approval → total_loans_outstanding + gross_approved_amount
(or + selected.requested_amount for a single selected CRE app).
existing_cre_exposure = sum of loan_type=CRE loan balances, not the sector-exposures
table (they diverge for grandfathered/mixed sectors).
variance_bps from the UNROUNDED ratio, then round 2dp. Computing from the 4dp-rounded
ratio is a precision bug.
- Signed variance: branch worse-than-benchmark → positive bps (the common case for these
branches). Keep the sign; don't absolute-value.
post_approval_pct > limit_pct is over_limit (strict >). Exactly-at-limit is not
over.
- NCUA
state_metrics are bare integers, not 4dp ratios. delinquency_bps, loan_to_share_ pct, roaa_bps, positive_net_income_pct are reported as-is.
bank_capacity_used ≠ approved_amount for SBA/participation: SBA → multiply by
(1 - sba_guaranty_pct); participation → minus the sold portion. But concentration uses the
full approved/requested amount (the loan is originated in full).
severe_bucket_counts payment_status ordering is lexicographic on the enum string —
90+ Days Past Due comes before Current (digit before letter in ASCII), not by delinquency
severity.
priority_ranking excludes declines and defers — approved + conditional only.
conditions: ["none"] for plain approve and for decline/defer (conditions attach to
approval paths, not declines). Declines carry their reasons in decline_reasons, not in
conditions.
- GET only / never
/api/judge — the judge is not a public endpoint for you.
- Quote URLs with
?/& in zsh.
13. Output field & enum quick-reference (consolidated)
- payment_status:
Current | 30 Days Past Due | 60 Days Past Due | 90+ Days Past Due | Nonaccrual.
- recommended_action:
monitor | watchlist | special-assets | workout | partial_chargeoff_review | legal_referral.
- risk_class (CDFI):
Prime | Desirable | Satisfactory | Watch | Doubtful | Projected Loss.
- decision:
approve | conditional_approve | decline | defer | participation_required.
- conditions:
participation_required | reduced_amount | board_exception | sba_guaranty_required | startup_monitoring | none.
- handling (concentration_flags):
approve | conditional_approve | decline | participation_required | none.
- decline reason codes:
capacity_limit | sector_breach | weak_dscr | high_ltv | low_fico | recent_bankruptcy | startup_risk | underwater_collateral | policy_floor_missing | documentation_gap | fdic_adverse_variance | ncua_peer_weakness.
- CRE conditions:
bank_retained_exposure_cap | committee_cre_exception | updated_appraisal_before_close | tenant_roll_and_lease_review | minimum_dscr_covenant_1_25 | quarterly_financial_reporting | no_additional_cre_without_committee_review.
- posture:
continue_approving | continue_with_tighter_conditions | temporarily_pause.
- benchmark_version strings:
fdic_q4_2024, ncua_q1_2025.
- FDIC benchmark_metric values:
total_loans_noncurrent_pct | total_real_estate_noncurrent_pct | construction_development_noncurrent_pct | total_real_estate_30_89_pct (use only those the
template's enum allows).
14. Per-family execution checklist (transfer to unseen tasks)
- Parse the prompt for:
branch_id / segment_id, as-of date, target rating threshold,
specific application_ids, and which section family (regrade / allocation / segment-posture /
watch-list / competing-CRE).
GET /api/policies once and cache the rule tables (risk_rating, cdfi_factor_scores,
cre_weighted_score, stress, capacity_concentration).
- Pull the branch, latest-quarter metrics, loans (with the right
min_current_rating /
loan_type filter), applications, sector-exposures, and the relevant benchmark
(fdic_q4_2024 / ncua_q1_2025) — or the segment — as the family requires.
- Apply the rule sections above in order; compute every numeric field with the §1 precision
rules (remember: bps from unrounded ratio).
- Build the JSON object matching the template's required keys, ordering, enums, and precision
exactly. Sort every list per its template ordering clause. Strip any key not in the template.
- Re-check guardrails (§12): no total_assets denominator, no rounded-ratio bps, regrade-vs-
watchlist rating basis, null-factor skip, severe-delinquency floor,
over_limit strict >.
- Emit only the JSON object (no narrative outside it) unless the prompt allows commentary.
1---2name: fewshot-attempt-01-123description: Lending-Committee Credit-Risk Skill (task_group_011)4---5# Lending-Committee Credit-Risk Skill (task_group_011)67Executable experience for credit-risk / lending-committee committee packets built from the8shared **credit-office public REST API**. A solver sees only a task prompt + an9`answer_template.json` + this file + the live API. The tasks in this family always ask you to10pull branch / loan / application / policy / benchmark data, apply a small set of deterministic11credit rules, and emit a JSON object whose shape is fixed by the template.1213This is a *method* document, not a per-task answer key. Read the section map, apply the rules,14and let the template's enums/ordering/precision drive the output.1516---1718## 0. Environment SOP (READ FIRST)1920- **Base URL:** `<remote-env-url>` — all endpoints under `/api/`, JSON, no auth.21- **GET only.** The environment is read-only for you. Never POST/PUT. **Never call `/api/judge`**22 (no test-time judge is available; it is out of scope and not part of the public surface).23- Always `GET /api/health` once to confirm the service is up. Use `curl -s … | jq` to shape JSON.24- Quote any URL containing `?`/`&` in zsh (e.g. `curl -s "http://…/loans?min_current_rating=3"`),25 otherwise the shell tries to glob the `?` and the call returns nothing.26- `branch_id` values are uppercase (`REDWOOD`, `LAKEVIEW`, `SUMMIT`, `HARBOR`, … (uppercase; a task names its own target branch)).27 Segment ids look like `CIVIC_NC_FIRE_EMS`.2829### Endpoint inventory and what each feeds3031| Endpoint | What it returns | Feeds answer section |32| --- | --- | --- |33| `GET /api/manifest` | policy_version, benchmark versions, seed | `benchmark_version`, cross-checks |34| `GET /api/policies` | **the single source of all rules** (capacity/concentration, risk-rating tables, CDFI factor scores, CRE weighted-score weights, stress formulas) | every rule below |35| `GET /api/branches` | all branches; `?institution_type=bank\|credit_union` | branch discovery |36| `GET /api/branches/{branch_id}` | `lending_capacity_q1`, `sector_ceiling_pct`, `cre_policy_limit_pct`, `total_assets`, `state_code`, `institution_type`, `fdic_benchmark_set` | capacity, concentration ceilings, CRE limit |37| `GET /api/branches/{branch_id}/metrics` | list by quarter; **latest quarter** row has `total_loans_outstanding`, `nonperforming_loans`, `delinquency_30_plus_pct`, `allowance_for_loan_losses`, `net_charge_offs`, `total_deposits` | NPA ratio, FDIC variance, concentration denominators |38| `GET /api/branches/{branch_id}/loans` | loans; filters `?loan_type=`, `?payment_status=`, `?min_current_rating=` | regrade population, watch-list, CRE exposure |39| `GET /api/branches/{branch_id}/sector-exposures` | per-sector `current_exposure`, `limit_pct`, `grandfathered` | sector concentration, flags |40| `GET /api/branches/{branch_id}/applications` | pending applications; `?loan_type=` | allocation decisions, CRE comparison |41| `GET /api/benchmarks/fdic/q4-2024` | FDIC ratios (`total_loans_noncurrent_pct`, `total_real_estate_noncurrent_pct`, `total_real_estate_30_89_pct`, `construction_development_*`) | FDIC benchmark variance |42| `GET /api/benchmarks/ncua/q1-2025` | rows per state + a `US` aggregate; `?state_code=` filters | credit-union segment state metrics, peer comparison |43| `GET /api/credit-union-segments/{segment_id}` | `state_code`, `peer_states`, `quarterly_capacity`, `current_outstanding`, `minimum_checklist`, `risk_tolerance`, `internal_context`, `notes` | segment posture page |4445**Query-param semantics:** `?min_current_rating=N` returns loans with `current_rating >= N`46(ratings are worse-as-they-grow: 1 best … 8 worst). `?loan_type=CRE` and `?payment_status=`47are exact-match filters. `?state_code=NC` returns a single-row `rows` array.4849### Metric-minding rules50- `metrics` is a **list** keyed by `quarter` (e.g. `2025Q1`, `2024Q4`). Always pick the51 **latest** (highest) quarter for the as-of date in the prompt.52- `sector-exposures` may carry a per-sector `limit_pct` override (e.g. Healthcare 0.19 where the53 branch default `sector_ceiling_pct` is 0.21). Use the per-row `limit_pct`, falling back to54 `branch.sector_ceiling_pct` for sectors not listed.55- `loans` carry nullable factors: `dscr`, `ltv`, `fico`, `debt_to_asset`, `liquidity_months`,56 `collateral_value` can all be `null`. Handle nulls explicitly (see §3).5758---5960## 1. Numeric & ordering conventions (apply everywhere)6162- **Currency:** 2 decimals (USD), round half-up.63- **Ratios / percentages / concentrations:** 4 decimals.64- **Basis points (variance_bps, delinquency_bps):** 2 decimals … **except** NCUA65 `state_metrics` values which are reported as bare integers exactly as the benchmark table66 gives them (see §6).67- **`variance_bps` is computed from the UNROUNDED `variance_ratio`, then rounded to 2dp.** Do not68 compute bps from the already-4dp-rounded ratio (that loses precision — e.g. NPA 1037.49 vs the69 1037.0 you would get from the rounded 0.1037). Pattern:70 `variance_ratio = branch_ratio - benchmark_ratio` (unrounded);71 `variance_bps = variance_ratio * 10000` (round 2dp);72 then publish `variance_ratio` rounded 4dp.73- `signed` variance: branch ratio **minus** benchmark ratio. A branch worse than benchmark yields74 a **positive** variance_bps (more delinquency / more NPA = bad). Keep the sign.75- **List ordering** is always explicit in the template — read it. Common orderings: ascending76 `loan_id`; ascending `final_rating`; ascending `action`/`sector`/`payment_status` (lexicographic77 on the enum string); ascending `trigger_id`; ascending `current_rating` then `payment_status`;78 descending `exposure` then ascending `loan_id` (workout queue); ascending `application_id`;79 ascending alphabetic for reason-code lists.80- All `loan_ids` arrays are sorted ascending `loan_id` (string sort).8182---8384## 2. Risk-rating re-derivation (regrade) — `/api/policies` → `risk_rating`8586The regrade target population is **loans with `current_rating >= target_current_rating_min`**87(default min = 3, i.e. "rated 3 or worse"). Pull them with88`GET /branches/{id}/loans?min_current_rating=3`.8990### Factor → rating tables (from policy `risk_rating`)91Apply each *available* factor; **null factors are skipped** (they do not contribute a rating).9293- **DSCR** (`dscr`): `>=1.5→3`, `>=1.25→4`, `>=1.05→5`, `>=1.0→6`, `<1.0→7`.94- **LTV** (`ltv`): `<=0.65→3`, `<=0.75→4`, `<=0.85→5`, `<=1.0→6`, `>1.0→7`.95- **Delinquency floor** (`payment_status` → minimum rating, the "severe-delinquency override"):96 `Current→null`, `30 Days Past Due→4`, `60 Days Past Due→5`, `90+ Days Past Due→7`,97 `Nonaccrual→8`.9899### Dominant-factor (worst-notch) rule100> `final_rating = max(available factor ratings)` — the **worst (highest numeric)** rating101> produced by DSCR, LTV, and the delinquency floor wins.102103- The delinquency floor acts as a hard floor: a `Nonaccrual` loan is **8** no matter how strong104 DSCR/LTV are; a `90+ Days Past Due` loan is **≥7**.105- If **all** factor-derived ratings are null/absent (e.g. an unsecured consumer loan with no DSCR,106 no LTV, and `Current`), **retain the `current_rating`** (no re-derivation possible). You can107 only worsen or hold — never improve — during a regrade.108- `material_downgrade_notches = 2` (policy). A loan whose `final_rating - current_rating >= 2`109 is a **material downgrade**.110111### Regrade outputs (per template, e.g. train_001 shape)112- `target_current_rating_min`, `target_loan_count`, `target_exposure` = sum of113 `outstanding_balance` over the target population.114- `final_rating_exposure_totals`: group **all** target loans by `final_rating`; one row per115 final_rating present, ordered ascending `final_rating`, with `loan_count` and `exposure`.116- `migration_from_current_rating_3`: among target loans whose **`current_rating == 3`** (the117 threshold entry point), group those that actually **moved** (final_rating > 3) by `final_rating`,118 with `loan_ids`. Loans that stayed at 3 are shown in the totals but **omitted** from this119 migration list (it is a *migration* — only movers).120- `material_downgrades`: every target loan with `downgrade_notches >= 2`, fields121 `{loan_id, current_rating, final_rating, downgrade_notches, exposure}`, ordered ascending122 `loan_id`. (downgrade_notches = final - current.)123- `top_problem_credit`: the single loan with the **worst final_rating** (ties → largest124 exposure, then lowest loan_id). Include `borrower_name`, `payment_status`, and a125 `recommended_action` per §10.126127### Watch-list action coverage (regrade follow-up)128Subset of regraded loans needing follow-up = those with `final_rating >= 6`. Group by129`recommended_action` (§10 mapping). `covered_loan_count`/`covered_exposure` = totals over that130subset; `by_action` ordered ascending by `action` enum string, `loan_ids` ascending.131132> **Regrade vs watch-list exclusion:** the *regrade* population is `current_rating >= 3` and133> uses **final_rating** for action mapping; the *watch-list* packet (§9) uses134> `current_rating >= 6` (adverse) with **no regrade** (actions keyed off current_rating). Don't135> mix the two rating bases.136137---138139## 3. NPA / FDIC benchmark variance140141NPA review (train_001-style) uses benchmark metric `total_loans_noncurrent_pct` (the template142also allows `total_real_estate_noncurrent_pct`, `construction_development_noncurrent_pct`).143144```145benchmark_version = "fdic_q4_2024" # from branch.fdic_benchmark_set / /api/manifest146benchmark_metric = one of the template's allowed values (context: NPA → total_loans_noncurrent_pct)147branch_npa_exposure = metrics[latest].nonperforming_loans148branch_total_loans = metrics[latest].total_loans_outstanding149branch_npa_ratio = branch_npa_exposure / branch_total_loans # 4dp150fdic_benchmark_ratio = FDIC[benchmark_metric] # 4dp151variance_ratio = branch_npa_ratio - fdic_benchmark_ratio # 4dp (unrounded for bps)152variance_bps = variance_ratio * 10000 # 2dp, from unrounded ratio153```154155### Metric → branch-ratio source mapping (pick by benchmark_metric)156- `*_noncurrent_pct` metrics → branch ratio = `nonperforming_loans / total_loans_outstanding`.157- `*_30_89_pct` metrics → branch ratio = `metrics[latest].delinquency_30_plus_pct` (use the field158 directly, already a ratio).159160A CRE-concentration packet (train_005-style) instead pairs161`fdic_benchmark_metric = total_real_estate_30_89_pct` with162`branch_delinquency_ratio = metrics[latest].delinquency_30_plus_pct`. Same variance math; the163branch ratio field name in that template is `branch_delinquency_ratio` / `fdic_variance_*`.164165---166167## 4. Lending capacity & allocation (train_002-style)168169For a branch's pending-applications allocation packet:170171```172lending_capacity_q1 = branch.lending_capacity_q1 (from /branches/{id})173gross_approved_amount = sum of approved_amount over APPROVED + CONDITIONAL_APPROVE apps174committed_capacity_amount = sum of bank_capacity_used over those same apps175remaining_capacity = lending_capacity_q1 - committed_capacity_amount176```177178### Decision enum (`approve | conditional_approve | decline | defer | participation_required`)179Conditions enum: `participation_required | reduced_amount | board_exception |180sba_guaranty_required | startup_monitoring | none`.181182**Per-application decision logic (apply in priority order):**1831. **Hard decline reasons first** (§8): if any reason code like `low_fico`, `recent_bankruptcy`,184 `underwater_collateral`, `policy_floor_missing`, `documentation_gap` fires → `decline`.1852. **Credit weakness decline**: `high_ltv` AND `weak_dscr` together, or `high_ltv` AND186 `startup_risk`, generally → `decline`. A single borderline weakness with strong offsets187 (e.g. high FICO) may still approve.1883. **Capacity**: after higher-priority apps are funded, if remaining capacity can't absorb the189 app and no other weakness exists → `decline` with reason `capacity_limit`.1904. **Sector/CRE concentration approach** (post_approval pct near or over the sector `limit_pct`)191 → do not decline outright; mitigate via **`conditional_approve`** with condition192 `participation_required` (sell the excess to participants) **or** `reduced_amount`193 (approve a smaller amount) **or** `board_exception` (committee override).1945. **SBA-guaranteed apps** (`sba_guaranty_pct` present) → `conditional_approve` with195 `sba_guaranty_required`; add `startup_monitoring` when `years_in_business` is short (< ~2y).1966. Otherwise → `approve` (condition `none`).197198### approved_amount vs bank_capacity_used199- **plain `approve`**: `approved_amount = requested_amount`; `bank_capacity_used = approved_amount`.200- **`sba_guaranty_required`**: `approved_amount = requested_amount`;201 `bank_capacity_used = approved_amount * (1 - sba_guaranty_pct)` (the SBA-guaranteed share is202 not bank risk/capacity).203- **`participation_required`**: `approved_amount = requested_amount` (the full loan is204 originated); `bank_capacity_used = approved_amount − participated_portion` (the sold portion205 relieves bank capacity). The participated portion is the amount needed to bring the relevant206 sector/large-exposure back under its ceiling. When LTV>1 / underwater, the bank may also retain207 only a capped share.208- **`reduced_amount`**: `approved_amount = min(requested, capacity/headroom)`; `bank_capacity_used209 = approved_amount`.210- **`decline`/`defer`**: `approved_amount = 0.0`, `bank_capacity_used = 0.0`, `conditions =211 ["none"]`.212213### priority_ranking214Ordered list of `application_id`, **highest priority first, approved + conditional_approve only**215(declines/defers excluded). Priority is committee priority — strategic relationship/quality first,216then core approves, then conditional — **not** simply by amount. Reverse it for tie-breaks if the217template asks ascending.218219---220221## 5. Concentration (sector ceilings & CRE limit)222223### Sector concentration224- Per-sector `limit_pct` comes from `/sector-exposures`; default to `branch.sector_ceiling_pct`225 for sectors absent from that table.226- `grandfathered=1` sectors may sit over ceiling but **new approvals may not worsen** them without227 mitigation (policy `capacity_concentration.grandfathering_note`).228- Allowed mitigations: `participation_required`, `reduced_amount`, `board_exception`.229230### Post-approval concentration denominator (IMPORTANT — do not use total_assets)231- **"existing" concentration** (e.g. CRE): `exposure / total_loans_outstanding`.232- **"post_approval" concentration** (sector view after funding approved apps):233 `exposure_after_approval = existing_sector_exposure + approved_amount(s) in that sector`;234 `post_approval_pct = exposure_after_approval / (total_loans_outstanding + gross_approved_amount)`.235 i.e. the post-approval loan book = current book **plus** the gross new approvals.236- For a single selected CRE app (train_005): `selected_post_approval_cre_concentration =237 (existing_cre_exposure + selected.requested_amount) / (total_loans_outstanding +238 selected.requested_amount)` — the **full requested** amount is added (participation is a239 decision/condition, not a concentration reducer; the bank still originates the full loan).240241`over_limit` = `post_approval_pct > limit_pct` (strict `>`; a value exactly at the limit is not242over).243244### `existing_cre_exposure` derivation245Sum of `outstanding_balance` over `GET /branches/{id}/loans?loan_type=CRE`. (This is the246loan_type=CRE aggregation, **not** the sector-exposures table — the two differ for grandfathered/247mixed sectors.)248249### `concentration_flags` (per approved app touching a sector)250One row per approved/conditional app whose sector post-approval pct **approaches or exceeds** the251sector `limit_pct` (within a thin headroom, e.g. ~0.01, or over). Fields: `sector`,252`application_id`, `limit_pct`, `post_approval_pct` (4dp), `flag` (bool), `handling` (enum:253`approve | conditional_approve | decline | participation_required | none`). Sort by `sector` then254`application_id`.255256### CRE policy variance (train_005)257```258cre_policy_limit_pct = branch.cre_policy_limit_pct259existing_cre_exposure = sum(CRE loan balances)260existing_cre_concentration = existing_cre_exposure / total_loans_outstanding # 4dp261selected_post_approval_cre_concentration = (existing + requested) / (total_loans + requested) # 4dp262selected_policy_variance_bps = (selected_post - cre_policy_limit_pct) * 10000 # 2dp, unrounded263```264265---266267## 6. Credit-union segment posture (train_003-style)268269Inputs: `GET /credit-union-segments/{segment_id}` + `GET /api/benchmarks/ncua/q1-2025` (+ optional270`?state_code=`).271272### state_metrics273Take the segment's `state_code` row from the NCUA benchmark. Report **integer values exactly as274in the table** (no rounding, no /100): `delinquency_bps`, `loan_to_share_pct`, `roaa_bps`,275`positive_net_income_pct`. `benchmark_version = "ncua_q1_2025"`. (Contrast: ratio fields elsewhere276are 4dp — these NCUA fields are integers.)277278### peer_comparison279- `peer_states` = segment.`peer_states` (use verbatim, then sort ascending state code).280- `nc_vs_us` and `nc_vs_peer_median`: for each of the four metrics, direction is281 `"higher" | "lower" | "equal"` comparing the segment state's value to (a) the `US` aggregate row282 and (b) the **median** of the `peer_states` rows.283- Note: for `delinquency_bps` higher = worse; for `roaa_bps`/`positive_net_income_pct` higher =284 better; for `loan_to_share_pct` higher = more leveraged. The direction is a pure numeric285 comparison (higher/lower), the *interpretation* is separate. Compute medians with the usual286 middle-of-three for the 3 peer rows.287288### posture (enum: `continue_approving | continue_with_tighter_conditions | temporarily_pause`)289Decision matrix driven by `segment.notes`, `risk_tolerance`, `internal_context`, and the290direction matrix:291- Capacity available **and** external risk no worse than national/peers → `continue_approving`.292- Capacity available **but** external risk weaker (NC higher delinquency, lower ROAA, lower293 positive-net-income vs both US and peer median) → **`continue_with_tighter_conditions`** (add294 operating controls, don't pause).295- Capacity exhausted **or** external risk severely adverse / control breakdown →296 `temporarily_pause`.297298### controls299- `required_checklist_gates` = segment.`minimum_checklist` (verbatim set from the endpoint), drawn300 from enum: `board_authorization, equipment_invoice, fleet_replacement_plan,301 payer_contract_summary, public_contract_or_tax_support, proof_of_insurance, ucc_or_title_lien`.302- `added_operating_controls` derive from `internal_context`:303 - insurance/lien control issue → `pre_close_insurance_binder_verification`,304 `lien_perfection_prior_to_funding`;305 - staffing/senior-underwriter constraint → `senior_underwriter_second_review`;306 - recent delinquency watch → `monthly_segment_delinquency_watch`;307 - external state monitoring → `quarterly_state_benchmark_monitoring`;308 - capacity overrun possibility → `committee_exception_for_capacity_overrun`.309 Enum: `pre_close_insurance_binder_verification, lien_perfection_prior_to_funding,310 senior_underwriter_second_review, quarterly_state_benchmark_monitoring,311 monthly_segment_delinquency_watch, committee_exception_for_capacity_overrun`. Sort the set.312313### escalation_triggers314List of `{trigger_id, condition, owner}`, ordered ascending `trigger_id` (ET001, ET002, …).315Select conditions relevant to the segment's risk profile from: `segment_recent_delinquency_ge_31690_bps, missing_insurance_or_lien_exception, quarterly_capacity_exceeded_or_exception_requested,317state_delinquency_gap_widens_25_bps`. Owner mapping:318- `segment_recent_delinquency_ge_90_bps` → `credit_risk_manager`319- `missing_insurance_or_lien_exception` → `operations_control_manager`320- `quarterly_capacity_exceeded_or_exception_requested` → `lending_committee_chair`321322(Include a trigger as a standing escalation gate for the segment even if the current value is just323below its threshold — they are armed gates, not only currently-breached ones. Drop a trigger only324when the segment's profile makes it irrelevant — e.g. omit `state_delinquency_gap_widens_25_bps`325when the state gap is already the dominant, not a widening-delta, risk.)326327### interpretation328- `capacity_status`: `capacity_available | capacity_constrained | no_capacity` — based on329 quarterly_capacity headroom vs current origination flow (capacity_available when the quarterly330 budget is not exhausted; do **not** compare `current_outstanding` stock against `quarterly_331 capacity` flow).332- `external_risk_status`: `stronger_than_national_and_peers | mixed_vs_national_and_peers |333 weaker_than_national_and_peers` — from the nc_vs_us / nc_vs_peer_median direction matrix.334- `risk_tolerance`: take `segment.risk_tolerance` (`restrained | moderate | expansive`).335- `committee_message`: pick from `capacity_available_but_external_risk_weaker |336 pause_until_state_metrics_recover | routine_approval_path_supported` to match the posture.337338---339340## 7. CDFI-style risk classes & factor scoring (watch-list, train_004-style)341342From `policy.cdfi_factor_scores`: score each **available** factor (null → skip), sum the scores,343map the sum to a class. **Null factors are skipped, not zeroed.**344345### Factor score tables346| Factor | `<0.40`/`>720`/`>12` | `0.40-0.60`/`680-720`/`6-12` | `0.60-0.80`/`580-679`/`3-6` | `>0.80`/`<580`/`<3` |347| --- | --- | --- | --- | --- |348| `ltv` | 0 | 2 | 4 | 6 |349| `debt_to_asset` | 0 | 2 | 4 | 6 |350| `fico` (`>720`→0, `680-720`→1, `580-679`→3, `<580`→5) | 0 | 1 | 3 | 5 |351| `liquidity_months` (`>12`→0, `6-12`→1, `3-6`→3, `<3`→5) | 0 | 1 | 3 | 5 |352353(LTV/DTA bands are value ranges; FICO/liquidity are their own scales — use the table literally.)354355### Class mapping (policy `cdfi_factor_scores.classes`)356| factor_score sum | risk_class |357| --- | --- |358| 0-5 | `Prime` |359| 6-9 | `Desirable` |360| 10-13 | `Satisfactory` |361| 14-18 | `Watch` |362| >=19 | `Doubtful` |363| **>=14 AND `ltv > 1.0`** | **`Projected Loss`** (underwater-collateral override) |364365> The policy text states `Projected Loss = ">=19 and ltv>1.0"`, but the **applied** rule366> escalates any **Watch-or-worse** score (>=14) with `ltv > 1.0` (underwater collateral) to367> `Projected Loss`. Apply this override. (`ltv>1.0` is the "underwater collateral" trigger.)368369`watch_list_summary.risk_classes`: list of `{loan_id, risk_class, factor_score}` for the adverse370population, ordered **ascending `loan_id`**. `monitoring_cadence` = `monthly` for an adverse371watch-list (rating >= 6); `quarterly`/`semiannual` only for healthier populations.372373---374375## 8. Decline reason codes (train_002/005) — derive from application/loan factors376377Reason-code enum: `capacity_limit, sector_breach, weak_dscr, high_ltv, low_fico,378recent_bankruptcy, startup_risk, underwater_collateral, policy_floor_missing, documentation_gap,379fdic_adverse_variance, ncua_peer_weakness`.380381Trigger thresholds (apply to application fields):382- `high_ltv` — `ltv > 0.80` (for business/CRE; consumer may tolerate slightly more with strong FICO).383- `weak_dscr` — `dscr < 1.25` (below the "pass" pass rating 4 threshold). `dscr is None` on a384 product that doesn't use DSCR (consumer/mortgage) is **not** weak_dscr.385- `low_fico` — `fico < 580`.386- `recent_bankruptcy` — `bankruptcy_months_ago` present and `< ~24` months.387- `startup_risk` (decline) vs `startup_monitoring` (condition): `years_in_business < ~2` is388 startup; pair with strong credit → `startup_monitoring` condition on a conditional approve; pair389 with another weakness → `startup_risk` decline reason.390- `underwater_collateral` — `ltv > 1.0` (collateral value below loan).391- `capacity_limit` — the **only** reason when an otherwise-acceptable app is declined because Q1392 capacity is consumed by higher-priority apps.393- `sector_breach` — approving would push the sector (or CRE aggregate) over its `limit_pct`.394- `fdic_adverse_variance` — branch's FDIC benchmark variance is adverse (positive bps, branch395 worse than benchmark) for the relevant metric; an ambient reason code on CRE/NPA-sensitive apps.396- `ncua_peer_weakness` — credit-union segment where the state is weaker than national/peers.397- `policy_floor_missing` / `documentation_gap` — missing `documentation_complete` or required398 checklist gate.399400`decline_reasons` maps each **declined** `application_id` → sorted (ascending alphabetic) list of401reason codes. For "competing credit" tasks (train_005), `unselected_reason_codes` is restricted to402`sector_breach | weak_dscr | high_ltv | fdic_adverse_variance`. The `applications_compared[].reason_403codes` on a **defer** decision mirrors the same list. Always sort reason-code lists **ascending404alphabetically**.405406---407408## 9. Watch-list stress & workout (train_004-style)409410Adverse population = `current_rating >= 6` (parameter `adverse_rating_min`, default 6). Pull via411`GET /branches/{id}/loans?min_current_rating=6`. No regrade here — use `current_rating` directly.412413### +200bp DSCR stress (policy `stress`)414```415shock_label = "+200bp"416formula = stressed_dscr = dscr / (1 + 0.18) # watch_list_formula417breach_threshold = 1.0 # policy stress.coverage_breach_threshold418stressed_dscr = dscr / 1.18 # 2dp419breaches_threshold = stressed_dscr < 1.0 # strict <420```421`stress_results.results` lists **only loans with `dscr` available** (`dscr is not None`), ordered422ascending `loan_id`. `breach_loan_ids` = those with `breaches_threshold true`, ascending `loan_id`.423424### CRE dual stress (train_005-style) — when comparing CRE applications425```426formula = "dscr * 0.85 / 1.18" # policy cre_dual_stress_formula427 (= dscr * 0.85 / (1 + 0.18))428coverage_breach_threshold = 1.0429stressed_dscr = dscr * 0.85 / 1.18 # 2dp430breaches_threshold = stressed_dscr < 1.0431```432`stress.results` ordered ascending `application_id`.433434### workout_queue435All adverse loans, each `{loan_id, exposure, risk_class, payment_status, recommended_action,436projected_loss}`. Order **descending `exposure`, then ascending `loan_id`**.437- `exposure` = `outstanding_balance`.438- `risk_class` from §7.439- `recommended_action` from §10 (keyed off **`current_rating`** in watch-list packets).440- `projected_loss` = `true` iff `risk_class == "Projected Loss"`, else `false`.441442### severe_bucket_counts443Group the adverse population by `(current_rating, payment_status)`; one row per non-empty bucket.444Fields `{current_rating, payment_status, loan_count, exposure}`. Order **ascending445`current_rating`, then `payment_status` (lexicographic on the enum string — `"90+ Days Past Due"`446sorts before `"Current"` because `'9'(0x39)` < `'C'(0x43)`)**.447448---449450## 10. recommended_action mapping (the action enum)451452Enum: `monitor | watchlist | special-assets | workout | partial_chargeoff_review | legal_referral`.453454The primary mapping is by the **governing rating** (which is `final_rating` after a regrade, or455`current_rating` for a pure watch-list packet) combined with payment status:456457| governing rating | payment_status | recommended_action |458| --- | --- | --- |459| <= 5 | (any) | `monitor` |460| 6 | (any) | `watchlist` |461| 7 | (any) | `special-assets` |462| 8 | `Nonaccrual` | `partial_chargeoff_review` |463| 8 | `90+ Days Past Due` | `partial_chargeoff_review` (or `workout`) |464465(`workout` and `legal_referral` apply to active-restructuring / fraud-litigation situations; in466the train data the 6/7/8 mapping above covers standard adverse credits. `legal_referral` is467reserved for fraud/litigation indicators; `workout` for loans already in active restructuring.)468469`top_problem_credit.recommended_action` and each `by_action` bucket use this enum, sorted ascending470by action string in `by_action`.471472---473474## 11. Competing-CRE decision (train_005-style) synthesis475476Given two competing CRE applications at one branch:4774781. **Compute each app's weighted CDFI score** (policy `cre_weighted_score`):479 - Weights: `capacity 0.45, collateral_exposure 0.36, conditions 0.11, character 0.05,480 capital 0.03` (capacity + collateral dominate at 0.81 combined).481 - Score each of the 5 Cs 1-5 from application attributes (capacity~DSCR,482 collateral~LTV, capital~debt_to_asset = total_debt/total_assets, character~fico/483 co_guarantor_strength/years_in_business/prior_delinquencies, conditions~proposed_rate/484 sector/documentation/purpose). **Lower = better.**485 - `weighted_cdfi_score = Σ weight_c × score_c` (1 decimal).486 - `score_class`: `<=2.0 → approve_quality`, `<=3.0 → conditional`, `>3.0 → weak`.4872. **CRE dual stress** both apps (§9). Flag `weak_dscr` reason if `stressed_dscr < 1.0`.4883. **Reason codes** per app: `fdic_adverse_variance` (branch FDIC variance adverse — ambient on489 both), `sector_breach` if the app's sector/CRE would breach, `weak_dscr` if base or stressed490 DSCR weak, `high_ltv` if `ltv > 0.80`.4914. **Decision per app**: `approve | conditional_approve | decline | defer | participation_required`.492 - `approve_quality` score + no breach + stress passes → `approve`.493 - `conditional` score but CRE concentration breaches → `participation_required` (sell excess;494 keep the better credit).495 - `weak` score + stress breach → `defer` (collect more info / sponsor support), or `decline`496 if a hard reason (low_fico/recent_bankruptcy/underwater) fires.4975. **recommended_path**: `selected_application_id` = the stronger credit (lower weighted score,498 better stress, fewer reason codes). `path` = the selected app's decision enum.499 `unselected_application_id`, `unselected_disposition` ∈ `{decline, defer}` (defer when the500 credit is merely weak/not-yet-declinable; decline only on a hard reason). `unselected_reason_501 codes` (restricted enum §8) sorted ascending alphabetic.5026. **concentration** block per §5. `fdic_*` fields use `total_real_estate_30_89_pct` +503 `delinquency_30_plus_pct`.5047. **conditions** set from: `bank_retained_exposure_cap, committee_cre_exception,505 updated_appraisal_before_close, tenant_roll_and_lease_review, minimum_dscr_covenant_1_25,506 quarterly_financial_reporting, no_additional_cre_without_committee_review`. Choose the set507 matching the selected path (participation → `bank_retained_exposure_cap`; existing CRE already508 over policy limit → `committee_cre_exception` + `no_additional_cre_without_committee_review`;509 CRE deal → `updated_appraisal_before_close` + `tenant_roll_and_lease_review` +510 `minimum_dscr_covenant_1_25` + `quarterly_financial_reporting`). Sort ascending alphabetic.511512---513514## 12. Common misjudgments & exclusion rules (guardrails)515516- **Regrade population vs watch-list population are different.** Regrade = `current_rating >= 3`517 using **final_rating** for actions. Watch-list = `current_rating >= 6` using **current_rating**518 for actions. Do not apply final_rating to a watch-list packet or current_rating to a regrade's519 action mapping.520- **Severe-delinquency override**: `Nonaccrual` → rating 8 and `90+ Days Past Due` → rating ≥7 are521 **floors**, not suggestions. A Nonaccrual loan never gets a final_rating below 8 even with522 DSCR 2.0 and LTV 0.4.523- **Null factors are skipped, never zeroed** (regrade dominant-factor, CDFI factor_score). A loan524 with `dscr=None, ltv=None, payment_status=Current` keeps its `current_rating`.525- **migration_from_current_rating_3 lists only movers** (final_rating > 3) among loans whose526 current_rating == 3. Loans that stayed at 3 appear in `final_rating_exposure_totals` but not in527 the migration list.528- **material_downgrades** uses `>= 2` notches (policy `material_downgrade_notches`), across the529 entire regrade target population (not just current_rating==3 loans).530- **Concentration denominator is the loan book, never `total_assets`.** Existing →531 `total_loans_outstanding`; post-approval → `total_loans_outstanding + gross_approved_amount`532 (or `+ selected.requested_amount` for a single selected CRE app).533- **`existing_cre_exposure`** = sum of `loan_type=CRE` loan balances, **not** the sector-exposures534 table (they diverge for grandfathered/mixed sectors).535- **`variance_bps` from the UNROUNDED ratio**, then round 2dp. Computing from the 4dp-rounded536 ratio is a precision bug.537- **Signed variance**: branch worse-than-benchmark → positive bps (the common case for these538 branches). Keep the sign; don't absolute-value.539- **`post_approval_pct` > `limit_pct`** is `over_limit` (strict `>`). Exactly-at-limit is not540 over.541- **NCUA `state_metrics` are bare integers**, not 4dp ratios. `delinquency_bps`, `loan_to_share_542 pct`, `roaa_bps`, `positive_net_income_pct` are reported as-is.543- **`bank_capacity_used` ≠ `approved_amount`** for SBA/participation: SBA → multiply by544 `(1 - sba_guaranty_pct)`; participation → minus the sold portion. But concentration uses the545 **full approved/requested** amount (the loan is originated in full).546- **`severe_bucket_counts` payment_status ordering is lexicographic on the enum string** —547 `90+ Days Past Due` comes before `Current` (digit before letter in ASCII), not by delinquency548 severity.549- **`priority_ranking` excludes declines and defers** — approved + conditional only.550- **`conditions: ["none"]`** for plain `approve` and for `decline`/`defer` (conditions attach to551 approval paths, not declines). Declines carry their reasons in `decline_reasons`, not in552 `conditions`.553- **GET only / never `/api/judge`** — the judge is not a public endpoint for you.554- Quote URLs with `?`/`&` in zsh.555556---557558## 13. Output field & enum quick-reference (consolidated)559560- **payment_status**: `Current | 30 Days Past Due | 60 Days Past Due | 90+ Days Past Due |561 Nonaccrual`.562- **recommended_action**: `monitor | watchlist | special-assets | workout |563 partial_chargeoff_review | legal_referral`.564- **risk_class (CDFI)**: `Prime | Desirable | Satisfactory | Watch | Doubtful | Projected Loss`.565- **decision**: `approve | conditional_approve | decline | defer | participation_required`.566- **conditions**: `participation_required | reduced_amount | board_exception |567 sba_guaranty_required | startup_monitoring | none`.568- **handling** (concentration_flags): `approve | conditional_approve | decline |569 participation_required | none`.570- **decline reason codes**: `capacity_limit | sector_breach | weak_dscr | high_ltv | low_fico |571 recent_bankruptcy | startup_risk | underwater_collateral | policy_floor_missing |572 documentation_gap | fdic_adverse_variance | ncua_peer_weakness`.573- **CRE conditions**: `bank_retained_exposure_cap | committee_cre_exception |574 updated_appraisal_before_close | tenant_roll_and_lease_review | minimum_dscr_covenant_1_25 |575 quarterly_financial_reporting | no_additional_cre_without_committee_review`.576- **posture**: `continue_approving | continue_with_tighter_conditions | temporarily_pause`.577- **benchmark_version** strings: `fdic_q4_2024`, `ncua_q1_2025`.578- **FDIC benchmark_metric values**: `total_loans_noncurrent_pct | total_real_estate_noncurrent_pct579 | construction_development_noncurrent_pct | total_real_estate_30_89_pct` (use only those the580 template's enum allows).581582---583584## 14. Per-family execution checklist (transfer to unseen tasks)5855861. Parse the prompt for: `branch_id` / `segment_id`, as-of date, target rating threshold,587 specific application_ids, and which section family (regrade / allocation / segment-posture /588 watch-list / competing-CRE).5892. `GET /api/policies` once and cache the rule tables (risk_rating, cdfi_factor_scores,590 cre_weighted_score, stress, capacity_concentration).5913. Pull the branch, latest-quarter metrics, loans (with the right `min_current_rating` /592 `loan_type` filter), applications, sector-exposures, and the relevant benchmark593 (fdic_q4_2024 / ncua_q1_2025) — or the segment — as the family requires.5944. Apply the rule sections above in order; compute every numeric field with the §1 precision595 rules (remember: bps from unrounded ratio).5965. Build the JSON object matching the template's required keys, ordering, enums, and precision597 exactly. Sort every list per its template ordering clause. Strip any key not in the template.5986. Re-check guardrails (§12): no total_assets denominator, no rounded-ratio bps, regrade-vs-599 watchlist rating basis, null-factor skip, severe-delinquency floor, `over_limit` strict `>`.6007. Emit **only** the JSON object (no narrative outside it) unless the prompt allows commentary.