SKILL — Credit-Risk / Lending-Committee Decision Packets (task_group_011)
A self-contained SOP for producing committee-ready JSON answers against the
shared credit-office public REST API. Distilled from reasoning through five
training archetypes (Redwood rating-migration, Lakeview allocation, Civic CU
segment posture, Summit watch-list stress, Harbor competing-CRE). No gold
answers used — this is the method, not the answers.
0. Environment & ground rules
- Base URL:
<remote-env-url> (remote; no local env/ source, no DB).
- Read-only: GET only. No auth. Never call
/api/judge.
- Always JSON. Pipe through
jq. Health: GET /api/health. Catalog: GET /api/manifest.
- Policy version:
credit_policy_v2025Q1 (fetch GET /api/policies once — it holds
every numeric rule below). Benchmark versions: FDIC fdic_q4_2024, NCUA ncua_q1_2025.
- Generated/committee date =
2025-03-31 (use as review_date / packet date).
- Every answer must conform to that task's
input/payloads/answer_template.json.
Enums and orderings there are strict — do not invent values.
1. Remote API usage SOP — endpoints, keys, query params
| Endpoint |
Returns |
Key fields / gotchas |
GET /api/health |
status + record counts |
sanity check |
GET /api/manifest |
versions, seed 11011, endpoint list |
generated_at 2025-03-31T00:00:00Z |
GET /api/policies |
ALL business rules (see §2) |
single object; cache it |
GET /api/branches |
branch_id list (also returns CU segment ids) |
branch ids are UPPERCASE |
GET /api/branches/{id} |
one branch |
cre_policy_limit_pct, sector_ceiling_pct, lending_capacity_q1, state_code, institution_type, total_assets. total_loans_outstanding is NOT here (null) — get from /metrics |
GET /api/branches/{id}/metrics |
LIST by quarter |
take [0] (latest = 2025Q1). Fields: nonperforming_loans, total_loans_outstanding, delinquency_30_plus_pct, allowance_for_loan_losses, net_charge_offs, total_deposits, quarter |
GET /api/branches/{id}/loans |
loans |
?loan_type=, ?payment_status=, ?min_current_rating=. Loan fields: loan_id, current_rating, payment_status, dscr(nullable), ltv(nullable), debt_to_asset(nullable), fico(nullable), liquidity_months(nullable), outstanding_balance, sector, loan_type, borrower_name, collateral_value, annual_debt_service |
GET /api/branches/{id}/sector-exposures |
sector rows |
fields are sector, current_exposure, limit_pct, grandfathered (0/1) — NOT exposure/outstanding_balance/balance |
GET /api/branches/{id}/applications |
pending apps |
optional ?loan_type=. Fields: application_id, loan_type, requested_amount, dscr(nullable), ltv, fico(nullable), sector, bankruptcy_months_ago, documentation_complete, years_in_business, sba_guaranty_pct, purpose, proposed_rate, total_debt, total_assets |
GET /api/benchmarks/fdic/q4-2024 |
FDIC ratios |
total_loans_noncurrent_pct=0.0098, total_real_estate_noncurrent_pct=0.0121, total_real_estate_30_89_pct=0.0051, construction_development_noncurrent_pct=0.0076, construction_development_30_89_pct=0.0042 |
GET /api/benchmarks/ncua/q1-2025 |
NCUA rows |
optional ?state_code=NC. Row fields: state_code, delinquency_bps, loan_to_share_pct, positive_net_income_pct, roaa_bps. Always returns a rows[] array even for a single state |
GET /api/credit-union-segments/{id} |
CU segment |
segment_id, segment_name, state_code, current_outstanding, quarterly_capacity, risk_tolerance, peer_states[], minimum_checklist[], internal_context{recent_delinquency_bps, staffing_constraint, control_issue}, portfolio_focus[], notes |
Query-param semantics (critical)
?min_current_rating=N → returns loans with current_rating >= N (rating scale:
3 = best, 8 = worst; higher = worse). This is an inclusive lower bound on a
"worse-or-equal" scale.
?loan_type= exact match (e.g. CRE, C%26I → URL-encode & as %26).
?payment_status= exact match (spaces → %20).
?state_code= 2-letter, returns rows[] with one element.
Which endpoint feeds which answer section
- Redwood (001):
/branches/REDWOOD + /branches/REDWOOD/loans?min_current_rating=3 + /metrics[0] + /policies + /benchmarks/fdic/q4-2024.
- Lakeview (002):
/branches/LAKEVIEW + /applications + /sector-exposures + /metrics[0] + /policies.
- Civic (003):
/credit-union-segments/CIVIC_NC_FIRE_EMS + /benchmarks/ncua/q1-2025 (all rows + ?state_code=NC) + /policies.
- Summit (004):
/branches/SUMMIT + /loans?min_current_rating=6 + /metrics[0] + /policies.
- Harbor (005):
/branches/HARBOR + /applications?loan_type=CRE + /sector-exposures + /loans + /metrics[0] + /policies + /benchmarks/fdic/q4-2024.
2. Universal business rules (from GET /api/policies)
2.1 Risk-rating re-derivation (risk_rating)
Final re-derived rating = worst (highest numeric) of the available factor-derived
ratings. Rating scale 3 (best) → 8 (worst).
- DSCR → rating:
dscr >= 1.5 → 3; >= 1.25 → 4; >= 1.05 → 5; >= 1.0 → 6; < 1.0 → 7.
- LTV → rating:
ltv <= 0.65 → 3; <= 0.75 → 4; <= 0.85 → 5; <= 1.0 → 6; > 1.0 → 7.
- Delinquency minimum (hard floor):
Current → none; 30 DPD → 4; 60 DPD → 5; 90+ DPD → 7; Nonaccrual → 8.
- Dominant-factor rule:
final_rating = max( current_rating, dscr_rating(if DSCR), ltv_rating(if LTV), delinquency_floor(if status has one) ).
Treat current_rating itself as a floor so re-derivation only downgrades or holds,
never spuriously upgrades (a Current loan with strong DSCR stays; a 30-DPD loan at 5 is not
upgraded to 4 just because the delinquency floor is 4). When DSCR/LTV are null, that factor
is simply omitted.
- Severe-delinquency override:
90+ DPD → 7 and Nonaccrual → 8 are mandatory floors that
override even a strong DSCR (e.g. a Nonaccrual loan with DSCR 1.55 lands at 8, not 5).
- Material downgrade:
final_rating - current_rating >= risk_rating.material_downgrade_notches (= 2). Only these go in material_downgrades.
2.2 Stress formulas (stress)
coverage_breach_threshold = 1.0 (breach when stressed_dscr < 1.0).
- Watch-list (Summit):
stressed_dscr = dscr / (1 + 0.18) → dscr / 1.18. Shock label +200bp.
- CRE dual stress (Harbor):
stressed_dscr = dscr * 0.85 / (1 + 0.18) → dscr * 0.72034.
- Only loans/apps with a non-null DSCR appear in
stress_results. Loans with null DSCR
are excluded from the stress list (but stay in watch-list/workout). breaches_threshold is boolean.
breach_loan_ids = the subset where stressed_dscr < 1.0, ascending loan_id.
2.3 CDFI factor scoring & risk classes (cdfi_factor_scores)
Sum available factor subscores (each 0–6) → factor_score (integer). Skip null factors
(do not add a max penalty for missing data).
| Factor |
bands → score |
ltv |
<0.40→0, 0.40–0.60→2, 0.60–0.80→4, >0.80→6 |
debt_to_asset |
<0.40→0, 0.40–0.60→2, 0.60–0.80→4, >0.80→6 |
fico |
>720→0, 680–720→1, 580–679→3, <580→5 |
liquidity_months |
>12→0, 6–12→1, 3–6→3, <3→5 |
risk_class from factor_score:
Prime 0–5 · Desirable 6–9 · Satisfactory 10–13 · Watch 14–18
Doubtful >=19 · Projected Loss >=19 AND ltv > 1.0.
projected_loss (workout_queue boolean) = true iff the loan's risk_class == "Projected Loss"
(i.e. factor_score >= 19 and ltv > 1.0). Apply the rule literally even for Nonaccrual
loans that don't meet both conditions.
2.4 CRE weighted score (cre_weighted_score) — Harbor
Five-C weights: capacity 0.45, collateral_exposure 0.36, conditions 0.11,
character 0.05, capital 0.03 (sum 1.0). Map app/loan factors to the C's (collateral_exposure←ltv,
character←fico, capital←debt_to_asset, capacity/conditions←DSCR/sector-specific). Multiply each C's
factor score by its weight and sum → weighted_cdfi_score (1 dp, lower is better).
score_class: approve_quality if <= 2.0; conditional if <= 3.0; weak if > 3.0.
- The app with the lower weighted score (better DSCR + lower LTV) is the stronger credit.
2.5 Capacity & concentration (capacity_concentration)
- Q1 lending capacity =
branches.lending_capacity_q1 (USD).
- Single-sector default ceiling =
branches.sector_ceiling_pct (0.21–0.24). Per-sector overrides
live in /sector-exposures (limit_pct); CRE-related sectors carry limit_pct = cre_policy_limit_pct (0.29 at HARBOR).
- CRE policy limit =
branches.cre_policy_limit_pct (the field name — not cre_limit_pct).
- Concentration denominator =
/metrics[0].total_loans_outstanding (NOT total_assets, NOT branch sum of balances). Critical pitfall.
post_approval_pct = (sector current_exposure + approved app amount in that sector) / total_loans_outstanding, 4 dp.
over_limit / sector flag = post_approval_pct > limit_pct.
- Grandfathering: existing over-ceiling exposure may stay (grandfathered=1), but new approvals
may not worsen that sector without a mitigation. Allowed mitigations:
participation_required,
reduced_amount, board_exception. Non-mitigated breaches → decline or participation_required.
2.6 Decline reason codes & decision enums
Decline reason-code enum (12): 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. Sort ascending alphabetically.
Decision enum (5): approve, conditional_approve, decline, defer, participation_required.
Task-002 conditions enum (6): participation_required, reduced_amount, board_exception,
sba_guaranty_required, startup_monitoring, none. (Use none for clean approvals.)
Decision heuristics (Lakeview): approve if DSCR≥1.25, LTV≤0.85, fico≥680, no sector breach,
documentation_complete, within capacity; conditional_approve / reduced_amount when capacity-tight
or LTV high but creditable; decline for DSCR<1.0, LTV>1.0, recent bankruptcy, sector breach w/o mitigation,
documentation gap; defer for missing documentation that is curable; participation_required /
sba_guaranty_required for SBA loans or capacity overruns.
2.7 Watch-list action coverage & workout queue (action enum, 6)
monitor, watchlist, special-assets, workout, partial_chargeoff_review, legal_referral.
Action mapping by final risk:
- Current / held rating, factor Watch-or-better →
monitor or watchlist.
- Substandard (final 6) →
watchlist/special-assets.
- 90+ DPD / final 7 →
special-assets/workout.
- Nonaccrual / final 8 →
legal_referral (or partial_chargeoff_review if projected loss).
Workout queue (workout_queue) ordering: descending exposure, then ascending loan_id.
Watch-list action coverage (watch_list_action_coverage) is grouped by_action ascending by action,
each with loan_count, exposure, ascending loan_ids.
3. Field-name & data-source gotchas (verified against live data)
/metrics is a list — index [0] for the current quarter. Field is total_loans_outstanding,
branch record's same-named field is null.
- Branch CRE field =
cre_policy_limit_pct (0.29 Harbor). Do not query cre_limit_pct.
/sector-exposures balance field = current_exposure; ceiling field = limit_pct
(per-sector, may equal sector_ceiling_pct or cre_policy_limit_pct); grandfathered is 0/1.
- Loans/applications carry many nullable factor fields (
dscr, ltv, fico, debt_to_asset,
liquidity_months). Always null-check before scoring; null DSCR ⇒ exclude from stress list.
loan_type C&I must be URL-encoded C%26I when passed as a query param.
- NCUA endpoint always returns
{benchmark_version, rows:[...]} — even ?state_code=NC gives a
1-element rows[]. Do not assume a flat object.
- Rating scale direction: higher current_rating = worse.
min_current_rating=3 = "3 or worse"
(the whole regrade review set); min_current_rating=6 = "adverse/watch-list" (Summit).
nonperforming_loans (NPA exposure) is a USD field in /metrics; it is not a count.
nonperforming_loans at REDWOOD (1,725,000.00) equals the Nonaccrual loan balance — confirms NPA
exposure comes from the metrics row, not a hand recount.
4. Per-task output field map & orderings
Task 001 — Redwood rating migration (review_date = 2025-03-31)
Top-level: branch_id, review_date, portfolio_regrade, npa_benchmark, material_downgrades, top_problem_credit.
portfolio_regrade:
target_current_rating_min = 3 (the min_current_rating used).
target_loan_count / target_exposure = count and 2-dp USD sum of the rating>=3 population.
final_rating_exposure_totals: all regrade loans grouped by final_rating (asc), with loan_count, exposure.
migration_from_current_rating_3: only loans whose current_rating == 3 (literal 3 — subset, not the whole population!), grouped by final_rating (asc), each row {final_rating, loan_count, exposure, loan_ids(asc)}. Common trap: do not use the whole >=3 population here.
watch_list_action_coverage: {covered_loan_count, covered_exposure, by_action[]} — the downgraded loans needing follow-up, bucketed by the §2.7 action enum (asc by action).
npa_benchmark:
benchmark_version = "fdic_q4_2024".
benchmark_metric ∈ {total_loans_noncurrent_pct, total_real_estate_noncurrent_pct, construction_development_noncurrent_pct}. For Redwood (general bank) use total_loans_noncurrent_pct = 0.0098.
branch_npa_exposure = metrics[0].nonperforming_loans; branch_total_loans = metrics[0].total_loans_outstanding.
branch_npa_ratio = npa_exposure / total_loans (4dp); fdic_benchmark_ratio = 0.0098 (4dp).
variance_ratio = branch_npa_ratio − fdic_benchmark_ratio (4dp, signed).
variance_bps = variance_ratio × 10000 (2dp, signed; positive = adverse).
material_downgrades: loans where final_rating − current_rating >= 2, asc by loan_id; each
{loan_id, current_rating, final_rating, downgrade_notches, exposure}.
top_problem_credit: the single worst credit — pick by severe delinquency (Nonaccrual/90+), then
largest exposure, then largest downgrade. {loan_id, borrower_name, exposure, current_rating, final_rating, payment_status(enum), recommended_action(enum)}. (REDWOOD: the Nonaccrual CRE loan
whose balance equals nonperforming_loans is the natural pick.)
Task 002 — Lakeview allocation
Top-level: branch_id, allocation, decisions, concentration_flags, decline_reasons, post_approval_concentrations.
allocation: lending_capacity_q1 (branch), gross_approved_amount (sum of approve+conditional approved_amounts),
committed_capacity_amount, remaining_capacity = capacity − committed, priority_ranking
(application_ids highest priority first, approve+conditional only).
decisions: asc by application_id; each {application_id, decision(enum), approved_amount(2dp), bank_capacity_used(2dp), conditions(enum)}. Declined apps → approved_amount 0.00; bank_capacity_used 0.00.
concentration_flags: asc by (sector, application_id); each {sector, application_id, limit_pct(4dp), post_approval_pct(4dp), flag, handling(enum)}. handling: approve/conditional_approve/decline/participation_required/none.
decline_reasons: object mapping each declined application_id → sorted list of reason codes (§2.6 enum).
post_approval_concentrations: asc by sector; {sector, exposure_after_approval(2dp), post_approval_pct(4dp), limit_pct(4dp), over_limit(bool)}. Denominator = total_loans_outstanding.
Task 003 — Civic CU segment posture
Use NCUA rows for NC, US, and peer states (segment.peer_states = [SC, TN, VA]).
state_metrics: state_code="NC", benchmark_version="ncua_q1_2025", and the four NC integer values
exactly as reported (delinquency_bps=79, loan_to_share_pct=76, roaa_bps=44, positive_net_income_pct=76).
peer_comparison: peer_states asc ([SC,TN,VA]); nc_vs_us and nc_vs_peer_median each =
{delinquency_bps, loan_to_share_pct, roaa_bps, positive_net_income_pct} with values higher|lower|equal
(direction of NC vs comparator). Peer median = median of the 3 peer-state values per metric.
- NC vs US: delinquency higher, loan_to_share higher, roaa lower, positive_net_income lower.
- NC vs peer median: delinquency higher, loan_to_share higher, roaa lower, positive_net_income lower.
controls.required_checklist_gates = segment's minimum_checklist (board_authorization,
equipment_invoice, public_contract_or_tax_support, proof_of_insurance, ucc_or_title_lien — note
fleet_replacement_plan & payer_contract_summary are allowed by the template but NOT in this segment's
checklist, so omit them). added_operating_controls chosen from the template set to address the
segment's internal_context (insurance-binder control_issue → pre_close_insurance_binder_verification;
staffing_constraint → senior_underwriter_second_review; external risk → quarterly_state_benchmark_monitoring,
monthly_segment_delinquency_watch, lien_perfection_prior_to_funding).
escalation_triggers: asc by trigger_id; each {trigger_id, condition, owner} using the condition &
owner enums. Standing set of 4 triggers with owners among credit_risk_manager / operations_control_manager /
lending_committee_chair (segment_recent_delinquency_ge_90_bps → note NC bps 79 & segment recent 86 are
both < 90, so it is a standing trigger, not currently breached).
interpretation: capacity_status=capacity_available (segment quarterly_capacity 2.9M, notes say
available); external_risk_status=weaker_than_national_and_peers; risk_tolerance=moderate (from segment);
committee_message=capacity_available_but_external_risk_weaker. posture=continue_with_tighter_conditions.
Task 004 — Summit watch-list stress
Top-level: branch_id, watch_list_summary, stress_results, workout_queue, severe_bucket_counts.
watch_list_summary:
adverse_rating_min = 6 (the requested threshold; adverse = current_rating >= 6).
adverse_loan_count, adverse_balance (2dp) over the rating>=6 set.
risk_classes: asc by loan_id; each {loan_id, risk_class(enum §2.3), factor_score}.
monitoring_cadence ∈ {monthly, quarterly, semiannual} — pick by worst risk class present
(Nonaccrual/Projected-Loss → monthly; otherwise quarterly).
stress_results: shock_label ("+200bp"/"watch_list_+200bp"), breach_threshold=1.0,
results asc by loan_id for loans with DSCR only: {loan_id, base_dscr(2dp), stressed_dscr(2dp), breaches_threshold(bool)} using stressed_dscr = dscr/1.18. breach_loan_ids asc.
workout_queue: asc/desc by exposure desc, then loan_id asc: {loan_id, exposure(2dp), risk_class, payment_status(enum), recommended_action(enum §2.7), projected_loss(bool)}. Includes all adverse loans
(even those without DSCR).
severe_bucket_counts: asc by (current_rating, payment_status); each
{current_rating, payment_status, loan_count, exposure} summarizing the adverse population.
Task 005 — Harbor competing-CRE
applications_compared = the two competing CRE apps (the larger 9xx-series sentinel apps,
e.g. HAR-APP-901 / HAR-APP-902), asc by application_id. Each {application_id, weighted_cdfi_score(1dp), score_class(enum), decision(enum), reason_codes(sorted)}.
recommended_path: {selected_application_id, path(enum), unselected_application_id, unselected_disposition ∈ {decline, defer}, unselected_reason_codes(sorted, restricted to sector_breach/weak_dscr/high_ltv/fdic_adverse_variance)}. Select = lower weighted score (survives
the dual stress stressed_dscr = dscr*0.85/1.18 >= 1.0); the unselected typically breaches stress
and/or sector.
stress: formula ("cre_dual_stress"/"stressed_dscr = dscr * 0.85 / (1 + 0.18)"),
coverage_breach_threshold=1.0, results asc by application_id: {application_id, base_dscr(2dp), stressed_dscr(2dp), breaches_threshold(bool)}.
concentration: cre_policy_limit_pct(4dp = branch.cre_policy_limit_pct), existing_cre_exposure(2dp
= sum of CRE-sector current_exposure; CRE sectors are those whose limit_pct == cre_policy_limit_pct),
existing_cre_concentration(4dp = exposure/total_loans_outstanding),
selected_post_approval_cre_concentration(4dp = (existing + selected requested_amount)/total_loans_outstanding),
selected_policy_variance_bps(2dp signed = (post − limit)×10000),
fdic_benchmark_metric=total_real_estate_30_89_pct, branch_delinquency_ratio(4dp, branch analog of that metric — derive from metrics/loans), fdic_benchmark_ratio=0.0051(4dp),
fdic_variance_ratio(4dp signed = branch − benchmark), fdic_variance_bps(2dp signed = ratio×10000).
conditions: subset of the 7 allowed enum values, asc alphabetically (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).
5. Numeric conventions
- Money / USD: round to 2 decimals. Sum balances/exposures with 2-dp rounding at the end.
- Ratios / concentrations / percentages expressed as ratios: 4 decimals (e.g. 0.1135).
- Basis-point variances: 2 decimals, signed (positive = adverse / over-limit).
variance_bps = (branch_ratio − benchmark_ratio) × 10000.
- DSCR (base & stressed): 2 decimals. weighted_cdfi_score: 1 decimal (lower better).
- Counts, ratings, factor_score: integers.
- breach / over_limit / projected_loss: JSON booleans (
true/false).
- Always honour the precision stated in each template field — it is checked.
6. Anticipated misjudgments & exclusion rules
- Regrade population vs watch-list. Redwood regrades
current_rating >= 3 (min_current_rating=3);
Summit watch-list is current_rating >= 6. Don't confuse the two thresholds. Within Redwood,
target_current_rating_min=3 covers almost the whole book; the migration block is the
current_rating == 3 subset only.
- Severe-delinquency override. 90+ DPD forces ≥7, Nonaccrual forces 8, even if DSCR is strong.
Don't let a good DSCR "rescue" a Nonaccrual.
- No spurious upgrades.
final = max(current, factors) — re-derivation only downgrades/holds.
Do not upgrade a 5 to 4 because only the 30-DPD floor (4) is available.
- Concentration denominator. Always
total_loans_outstanding from /metrics[0]. Never total_assets
(the branch record's, ~5× larger) — that would shrink every concentration % and hide breaches.
- NPA exposure source. Use
metrics[0].nonperforming_loans (USD), not a manual count, not total_assets.
- Stress-list DSCR filter. Loans with null DSCR are excluded from
stress_results but still appear in
workout_queue / watch_list_summary. Don't drop them from the portfolio.
final_rating_exposure_totals vs migration_from_current_rating_3. Former = all rating>=3 loans by
final_rating; latter = only current_rating==3 loans by final_rating (and includes loan_ids).
- Orderings.
ascending loan_id is string sort ("RED-LN-001" < "RED-LN-011" < "RED-LN-015" <
"RED-LN-901"). workout_queue is exposure-desc then loan_id-asc. applications_compared,
material_downgrades, risk_classes, breach_loan_ids, severe_bucket_counts,
post_approval_concentrations — each has its own stated ordering; follow the template literally.
- Reason-code lists sorted ascending alphabetically;
unselected_reason_codes is restricted to the
4-value subset, not the full 12.
- CRE sectors. Identify CRE sectors as those whose
/sector-exposures limit_pct equals
cre_policy_limit_pct (0.29), not by name heuristics. Respect grandfathered — don't worsen a
grandfathered over-ceiling sector without a mitigation.
- NCUA shape. Always
.rows[]; a ?state_code= call returns a 1-element rows array, not a flat object.
benchmark_metric choice. Match the FDIC metric to the branch/sector mix: general bank NPA review →
total_loans_noncurrent_pct; CRE delinquency variance (Harbor) → total_real_estate_30_89_pct;
construction-heavy → construction metrics. The template enum pins Harbor to total_real_estate_30_89_pct.
- Two competing apps, not all CRE apps. Harbor has several CRE-typed applications; the "competing"
pair is the two large
9xx-series sentinel apps. Confirm via amounts / the prompt's "two CRE requests".
score_class thresholds use <= (approve_quality ≤2.0, conditional ≤3.0, weak >3.0). Lower
weighted score is better; pick the lower score as selected.
recent_bankruptcy trigger: years_in_business small + bankruptcy_months_ago present →
recent_bankruptcy/startup_risk. low_fico only when fico is non-null and below band.
documentation_gap only when documentation_complete is false.
7. Step-by-step derivation SOP (apply per task)
- Read the prompt → identify branch_id / segment_id, the threshold param (
min_current_rating),
and which benchmark (FDIC vs NCUA) applies. Open the answer_template; list every required key, enum,
and ordering.
- Fetch
/api/policies once. Fetch /api/branches/{id}, /api/branches/{id}/metrics (take [0]),
the relevant loans/applications/sector-exposures with the right ? filter, and the benchmark.
- Derive per-field using §2 rules. Compute factor-derived ratings, stress DSCRs, factors scores,
concentrations, and variances with the exact formulas and precisions in §5.
- Filter & group with the exact subset rules (regrade pop vs migration subset; DSCR-only stress list;
adverse rating>=6; decline-only reason map; competing-pair apps).
- Sort each list to the template's ordering (string asc loan_id; exposure-desc; alpha asc reason codes;
asc by final_rating / sector / trigger_id / application_id).
- Round to the field's precision (money 2dp, ratios 4dp, bps 2dp signed, scores 1dp).
- Emit a single JSON object with exactly the required top-level keys and per-item keys; omit extras.
- Self-check against §6 pitfalls before finishing: denominator? NPA source? override applied? no
upgrade? enum exact? ordering literal? precision honoured?
1---2name: self-attempt-03-153description: SKILL — Credit-Risk / Lending-Committee Decision Packets (task_group_011)4---5# SKILL — Credit-Risk / Lending-Committee Decision Packets (task_group_011)67A self-contained SOP for producing committee-ready JSON answers against the8shared **credit-office public REST API**. Distilled from reasoning through five9training archetypes (Redwood rating-migration, Lakeview allocation, Civic CU10segment posture, Summit watch-list stress, Harbor competing-CRE). No gold11answers used — this is the *method*, not the answers.1213---1415## 0. Environment & ground rules1617- Base URL: `<remote-env-url>` (remote; no local `env/` source, no DB).18- Read-only: **GET only**. No auth. Never call `/api/judge`.19- Always JSON. Pipe through `jq`. Health: `GET /api/health`. Catalog: `GET /api/manifest`.20- Policy version: `credit_policy_v2025Q1` (fetch `GET /api/policies` once — it holds21 every numeric rule below). Benchmark versions: FDIC `fdic_q4_2024`, NCUA `ncua_q1_2025`.22- Generated/committee date = `2025-03-31` (use as `review_date` / packet date).23- Every answer must conform to that task's `input/payloads/answer_template.json`.24 Enums and orderings there are **strict** — do not invent values.2526---2728## 1. Remote API usage SOP — endpoints, keys, query params2930| Endpoint | Returns | Key fields / gotchas |31|---|---|---|32| `GET /api/health` | status + record counts | sanity check |33| `GET /api/manifest` | versions, seed `11011`, endpoint list | generated_at `2025-03-31T00:00:00Z` |34| `GET /api/policies` | ALL business rules (see §2) | single object; cache it |35| `GET /api/branches` | branch_id list (also returns CU segment ids) | branch ids are UPPERCASE |36| `GET /api/branches/{id}` | one branch | `cre_policy_limit_pct`, `sector_ceiling_pct`, `lending_capacity_q1`, `state_code`, `institution_type`, `total_assets`. **`total_loans_outstanding` is NOT here** (null) — get from `/metrics` |37| `GET /api/branches/{id}/metrics` | **LIST by quarter** | take `[0]` (latest = `2025Q1`). Fields: `nonperforming_loans`, `total_loans_outstanding`, `delinquency_30_plus_pct`, `allowance_for_loan_losses`, `net_charge_offs`, `total_deposits`, `quarter` |38| `GET /api/branches/{id}/loans` | loans | `?loan_type=`, `?payment_status=`, `?min_current_rating=`. Loan fields: `loan_id`, `current_rating`, `payment_status`, `dscr`(nullable), `ltv`(nullable), `debt_to_asset`(nullable), `fico`(nullable), `liquidity_months`(nullable), `outstanding_balance`, `sector`, `loan_type`, `borrower_name`, `collateral_value`, `annual_debt_service` |39| `GET /api/branches/{id}/sector-exposures` | sector rows | fields are `sector`, `current_exposure`, `limit_pct`, `grandfathered` (0/1) — **NOT** `exposure`/`outstanding_balance`/`balance` |40| `GET /api/branches/{id}/applications` | pending apps | optional `?loan_type=`. Fields: `application_id`, `loan_type`, `requested_amount`, `dscr`(nullable), `ltv`, `fico`(nullable), `sector`, `bankruptcy_months_ago`, `documentation_complete`, `years_in_business`, `sba_guaranty_pct`, `purpose`, `proposed_rate`, `total_debt`, `total_assets` |41| `GET /api/benchmarks/fdic/q4-2024` | FDIC ratios | `total_loans_noncurrent_pct`=0.0098, `total_real_estate_noncurrent_pct`=0.0121, `total_real_estate_30_89_pct`=0.0051, `construction_development_noncurrent_pct`=0.0076, `construction_development_30_89_pct`=0.0042 |42| `GET /api/benchmarks/ncua/q1-2025` | NCUA rows | optional `?state_code=NC`. Row fields: `state_code`, `delinquency_bps`, `loan_to_share_pct`, `positive_net_income_pct`, `roaa_bps`. Always returns a `rows[]` array even for a single state |43| `GET /api/credit-union-segments/{id}` | CU segment | `segment_id`, `segment_name`, `state_code`, `current_outstanding`, `quarterly_capacity`, `risk_tolerance`, `peer_states[]`, `minimum_checklist[]`, `internal_context{recent_delinquency_bps, staffing_constraint, control_issue}`, `portfolio_focus[]`, `notes` |4445### Query-param semantics (critical)46- `?min_current_rating=N` → returns loans with **`current_rating >= N`** (rating scale:47 **3 = best, 8 = worst**; higher = worse). This is an inclusive lower bound on a48 "worse-or-equal" scale.49- `?loan_type=` exact match (e.g. `CRE`, `C%26I` → URL-encode `&` as `%26`).50- `?payment_status=` exact match (spaces → `%20`).51- `?state_code=` 2-letter, returns `rows[]` with one element.5253### Which endpoint feeds which answer section54- **Redwood (001)**: `/branches/REDWOOD` + `/branches/REDWOOD/loans?min_current_rating=3` + `/metrics`[0] + `/policies` + `/benchmarks/fdic/q4-2024`.55- **Lakeview (002)**: `/branches/LAKEVIEW` + `/applications` + `/sector-exposures` + `/metrics`[0] + `/policies`.56- **Civic (003)**: `/credit-union-segments/CIVIC_NC_FIRE_EMS` + `/benchmarks/ncua/q1-2025` (all rows + `?state_code=NC`) + `/policies`.57- **Summit (004)**: `/branches/SUMMIT` + `/loans?min_current_rating=6` + `/metrics`[0] + `/policies`.58- **Harbor (005)**: `/branches/HARBOR` + `/applications?loan_type=CRE` + `/sector-exposures` + `/loans` + `/metrics`[0] + `/policies` + `/benchmarks/fdic/q4-2024`.5960---6162## 2. Universal business rules (from `GET /api/policies`)6364### 2.1 Risk-rating re-derivation (`risk_rating`)65Final re-derived rating = **worst (highest numeric)** of the available factor-derived66ratings. Rating scale 3 (best) → 8 (worst).6768- **DSCR → rating**: `dscr >= 1.5 → 3`; `>= 1.25 → 4`; `>= 1.05 → 5`; `>= 1.0 → 6`; `< 1.0 → 7`.69- **LTV → rating**: `ltv <= 0.65 → 3`; `<= 0.75 → 4`; `<= 0.85 → 5`; `<= 1.0 → 6`; `> 1.0 → 7`.70- **Delinquency minimum (hard floor)**: `Current → none`; `30 DPD → 4`; `60 DPD → 5`; `90+ DPD → 7`; `Nonaccrual → 8`.71- **Dominant-factor rule**: `final_rating = max( current_rating, dscr_rating(if DSCR), ltv_rating(if LTV), delinquency_floor(if status has one) )`.72 Treat `current_rating` itself as a floor so re-derivation **only downgrades or holds**,73 never spuriously upgrades (a Current loan with strong DSCR stays; a 30-DPD loan at 5 is not74 upgraded to 4 just because the delinquency floor is 4). When DSCR/LTV are null, that factor75 is simply omitted.76- **Severe-delinquency override**: `90+ DPD → 7` and `Nonaccrual → 8` are mandatory floors that77 override even a strong DSCR (e.g. a Nonaccrual loan with DSCR 1.55 lands at 8, not 5).78- **Material downgrade**: `final_rating - current_rating >= risk_rating.material_downgrade_notches` (= **2**). Only these go in `material_downgrades`.7980### 2.2 Stress formulas (`stress`)81- `coverage_breach_threshold` = **1.0** (breach when `stressed_dscr < 1.0`).82- **Watch-list (Summit)**: `stressed_dscr = dscr / (1 + 0.18)` → `dscr / 1.18`. Shock label `+200bp`.83- **CRE dual stress (Harbor)**: `stressed_dscr = dscr * 0.85 / (1 + 0.18)` → `dscr * 0.72034`.84- Only loans/apps **with a non-null DSCR** appear in `stress_results`. Loans with null DSCR85 are excluded from the stress list (but stay in watch-list/workout). `breaches_threshold` is boolean.86- `breach_loan_ids` = the subset where `stressed_dscr < 1.0`, **ascending loan_id**.8788### 2.3 CDFI factor scoring & risk classes (`cdfi_factor_scores`)89Sum available factor subscores (each 0–6) → `factor_score` (integer). **Skip null factors**90(do not add a max penalty for missing data).9192| Factor | bands → score |93|---|---|94| `ltv` | `<0.40→0`, `0.40–0.60→2`, `0.60–0.80→4`, `>0.80→6` |95| `debt_to_asset` | `<0.40→0`, `0.40–0.60→2`, `0.60–0.80→4`, `>0.80→6` |96| `fico` | `>720→0`, `680–720→1`, `580–679→3`, `<580→5` |97| `liquidity_months` | `>12→0`, `6–12→1`, `3–6→3`, `<3→5` |9899`risk_class` from `factor_score`:100- `Prime` 0–5 · `Desirable` 6–9 · `Satisfactory` 10–13 · `Watch` 14–18101- `Doubtful` `>=19` · `Projected Loss` `>=19 AND ltv > 1.0`.102103`projected_loss` (workout_queue boolean) = true iff the loan's `risk_class == "Projected Loss"`104(i.e. `factor_score >= 19` **and** `ltv > 1.0`). Apply the rule literally even for Nonaccrual105loans that don't meet both conditions.106107### 2.4 CRE weighted score (`cre_weighted_score`) — Harbor108Five-C weights: `capacity 0.45`, `collateral_exposure 0.36`, `conditions 0.11`,109`character 0.05`, `capital 0.03` (sum 1.0). Map app/loan factors to the C's (collateral_exposure←ltv,110character←fico, capital←debt_to_asset, capacity/conditions←DSCR/sector-specific). Multiply each C's111factor score by its weight and sum → `weighted_cdfi_score` (1 dp, **lower is better**).112- `score_class`: `approve_quality` if `<= 2.0`; `conditional` if `<= 3.0`; `weak` if `> 3.0`.113- The app with the **lower** weighted score (better DSCR + lower LTV) is the stronger credit.114115### 2.5 Capacity & concentration (`capacity_concentration`)116- Q1 lending capacity = `branches.lending_capacity_q1` (USD).117- Single-sector default ceiling = `branches.sector_ceiling_pct` (0.21–0.24). Per-sector overrides118 live in `/sector-exposures` (`limit_pct`); CRE-related sectors carry `limit_pct = cre_policy_limit_pct` (0.29 at HARBOR).119- CRE policy limit = `branches.cre_policy_limit_pct` (the field name — **not** `cre_limit_pct`).120- **Concentration denominator = `/metrics[0].total_loans_outstanding`** (NOT `total_assets`, NOT branch sum of balances). Critical pitfall.121- `post_approval_pct = (sector current_exposure + approved app amount in that sector) / total_loans_outstanding`, 4 dp.122- `over_limit` / sector `flag` = `post_approval_pct > limit_pct`.123- **Grandfathering**: existing over-ceiling exposure may stay (grandfathered=1), but **new approvals124 may not worsen** that sector without a mitigation. Allowed mitigations: `participation_required`,125 `reduced_amount`, `board_exception`. Non-mitigated breaches → decline or participation_required.126127### 2.6 Decline reason codes & decision enums128Decline reason-code enum (12): `capacity_limit`, `sector_breach`, `weak_dscr`, `high_ltv`,129`low_fico`, `recent_bankruptcy`, `startup_risk`, `underwater_collateral`, `policy_floor_missing`,130`documentation_gap`, `fdic_adverse_variance`, `ncua_peer_weakness`. Sort ascending alphabetically.131132Decision enum (5): `approve`, `conditional_approve`, `decline`, `defer`, `participation_required`.133134Task-002 conditions enum (6): `participation_required`, `reduced_amount`, `board_exception`,135`sba_guaranty_required`, `startup_monitoring`, `none`. (Use `none` for clean approvals.)136137Decision heuristics (Lakeview): `approve` if DSCR≥1.25, LTV≤0.85, fico≥680, no sector breach,138documentation_complete, within capacity; `conditional_approve` / `reduced_amount` when capacity-tight139or LTV high but creditable; `decline` for DSCR<1.0, LTV>1.0, recent bankruptcy, sector breach w/o mitigation,140documentation gap; `defer` for missing documentation that is curable; `participation_required` /141`sba_guaranty_required` for SBA loans or capacity overruns.142143### 2.7 Watch-list action coverage & workout queue (action enum, 6)144`monitor`, `watchlist`, `special-assets`, `workout`, `partial_chargeoff_review`, `legal_referral`.145146Action mapping by final risk:147- Current / held rating, factor Watch-or-better → `monitor` or `watchlist`.148- Substandard (final 6) → `watchlist`/`special-assets`.149- 90+ DPD / final 7 → `special-assets`/`workout`.150- Nonaccrual / final 8 → `legal_referral` (or `partial_chargeoff_review` if projected loss).151152Workout queue (`workout_queue`) ordering: **descending `exposure`, then ascending `loan_id`**.153Watch-list action coverage (`watch_list_action_coverage`) is grouped `by_action` ascending by action,154each with loan_count, exposure, ascending loan_ids.155156---157158## 3. Field-name & data-source gotchas (verified against live data)1591601. `/metrics` is a **list** — index `[0]` for the current quarter. Field is `total_loans_outstanding`,161 branch record's same-named field is **null**.1622. Branch CRE field = **`cre_policy_limit_pct`** (0.29 Harbor). Do not query `cre_limit_pct`.1633. `/sector-exposures` balance field = **`current_exposure`**; ceiling field = **`limit_pct`**164 (per-sector, may equal sector_ceiling_pct or cre_policy_limit_pct); `grandfathered` is 0/1.1654. Loans/applications carry many **nullable** factor fields (`dscr`, `ltv`, `fico`, `debt_to_asset`,166 `liquidity_months`). Always null-check before scoring; null DSCR ⇒ exclude from stress list.1675. `loan_type` `C&I` must be URL-encoded `C%26I` when passed as a query param.1686. NCUA endpoint always returns `{benchmark_version, rows:[...]}` — even `?state_code=NC` gives a169 1-element `rows[]`. Do not assume a flat object.1707. Rating scale direction: **higher current_rating = worse**. `min_current_rating=3` = "3 or worse"171 (the whole regrade review set); `min_current_rating=6` = "adverse/watch-list" (Summit).1728. `nonperforming_loans` (NPA exposure) is a USD field in `/metrics`; it is **not** a count.173 `nonperforming_loans` at REDWOOD (1,725,000.00) equals the Nonaccrual loan balance — confirms NPA174 exposure comes from the metrics row, not a hand recount.175176---177178## 4. Per-task output field map & orderings179180### Task 001 — Redwood rating migration (`review_date = 2025-03-31`)181Top-level: `branch_id, review_date, portfolio_regrade, npa_benchmark, material_downgrades, top_problem_credit`.182183`portfolio_regrade`:184- `target_current_rating_min` = 3 (the `min_current_rating` used).185- `target_loan_count` / `target_exposure` = count and 2-dp USD sum of the **rating>=3** population.186- `final_rating_exposure_totals`: **all** regrade loans grouped by `final_rating` (asc), with `loan_count`, `exposure`.187- `migration_from_current_rating_3`: **only loans whose `current_rating == 3`** (literal 3 — subset, not the whole population!), grouped by `final_rating` (asc), each row `{final_rating, loan_count, exposure, loan_ids(asc)}`. **Common trap:** do not use the whole >=3 population here.188- `watch_list_action_coverage`: `{covered_loan_count, covered_exposure, by_action[]}` — the downgraded loans needing follow-up, bucketed by the §2.7 action enum (asc by action).189190`npa_benchmark`:191- `benchmark_version` = "fdic_q4_2024".192- `benchmark_metric` ∈ {`total_loans_noncurrent_pct`, `total_real_estate_noncurrent_pct`, `construction_development_noncurrent_pct`}. For Redwood (general bank) use `total_loans_noncurrent_pct` = 0.0098.193- `branch_npa_exposure` = `metrics[0].nonperforming_loans`; `branch_total_loans` = `metrics[0].total_loans_outstanding`.194- `branch_npa_ratio` = npa_exposure / total_loans (4dp); `fdic_benchmark_ratio` = 0.0098 (4dp).195- `variance_ratio` = branch_npa_ratio − fdic_benchmark_ratio (4dp, **signed**).196- `variance_bps` = variance_ratio × 10000 (2dp, signed; positive = adverse).197198`material_downgrades`: loans where `final_rating − current_rating >= 2`, asc by `loan_id`; each199`{loan_id, current_rating, final_rating, downgrade_notches, exposure}`.200201`top_problem_credit`: the single worst credit — pick by severe delinquency (Nonaccrual/90+), then202largest exposure, then largest downgrade. `{loan_id, borrower_name, exposure, current_rating,203final_rating, payment_status(enum), recommended_action(enum)}`. (REDWOOD: the Nonaccrual CRE loan204whose balance equals `nonperforming_loans` is the natural pick.)205206### Task 002 — Lakeview allocation207Top-level: `branch_id, allocation, decisions, concentration_flags, decline_reasons, post_approval_concentrations`.208209`allocation`: `lending_capacity_q1` (branch), `gross_approved_amount` (sum of approve+conditional approved_amounts),210`committed_capacity_amount`, `remaining_capacity` = capacity − committed, `priority_ranking`211(application_ids **highest priority first**, approve+conditional only).212213`decisions`: asc by `application_id`; each `{application_id, decision(enum), approved_amount(2dp),214bank_capacity_used(2dp), conditions(enum)}`. Declined apps → `approved_amount` 0.00; `bank_capacity_used` 0.00.215216`concentration_flags`: asc by `(sector, application_id)`; each `{sector, application_id, limit_pct(4dp),217post_approval_pct(4dp), flag, handling(enum)}`. `handling`: approve/conditional_approve/decline/participation_required/none.218219`decline_reasons`: object mapping each **declined** application_id → sorted list of reason codes (§2.6 enum).220221`post_approval_concentrations`: asc by sector; `{sector, exposure_after_approval(2dp), post_approval_pct(4dp),222limit_pct(4dp), over_limit(bool)}`. Denominator = `total_loans_outstanding`.223224### Task 003 — Civic CU segment posture225Use NCUA rows for NC, US, and peer states (`segment.peer_states` = [SC, TN, VA]).226227`state_metrics`: `state_code`="NC", `benchmark_version`="ncua_q1_2025", and the four NC integer values228exactly as reported (delinquency_bps=79, loan_to_share_pct=76, roaa_bps=44, positive_net_income_pct=76).229230`peer_comparison`: `peer_states` asc ([SC,TN,VA]); `nc_vs_us` and `nc_vs_peer_median` each =231`{delinquency_bps, loan_to_share_pct, roaa_bps, positive_net_income_pct}` with values `higher|lower|equal`232(direction of NC vs comparator). Peer median = median of the 3 peer-state values per metric.233- NC vs US: delinquency higher, loan_to_share higher, roaa lower, positive_net_income lower.234- NC vs peer median: delinquency higher, loan_to_share higher, roaa lower, positive_net_income lower.235236`controls.required_checklist_gates` = segment's `minimum_checklist` (board_authorization,237equipment_invoice, public_contract_or_tax_support, proof_of_insurance, ucc_or_title_lien — note238fleet_replacement_plan & payer_contract_summary are allowed by the template but NOT in this segment's239checklist, so omit them). `added_operating_controls` chosen from the template set to address the240segment's `internal_context` (insurance-binder control_issue → pre_close_insurance_binder_verification;241staffing_constraint → senior_underwriter_second_review; external risk → quarterly_state_benchmark_monitoring,242monthly_segment_delinquency_watch, lien_perfection_prior_to_funding).243244`escalation_triggers`: asc by `trigger_id`; each `{trigger_id, condition, owner}` using the condition &245owner enums. Standing set of 4 triggers with owners among credit_risk_manager / operations_control_manager /246lending_committee_chair (`segment_recent_delinquency_ge_90_bps` → note NC bps 79 & segment recent 86 are247both < 90, so it is a *standing* trigger, not currently breached).248249`interpretation`: `capacity_status`=capacity_available (segment `quarterly_capacity` 2.9M, notes say250available); `external_risk_status`=weaker_than_national_and_peers; `risk_tolerance`=moderate (from segment);251`committee_message`=capacity_available_but_external_risk_weaker. `posture`=continue_with_tighter_conditions.252253### Task 004 — Summit watch-list stress254Top-level: `branch_id, watch_list_summary, stress_results, workout_queue, severe_bucket_counts`.255256`watch_list_summary`:257- `adverse_rating_min` = 6 (the requested threshold; adverse = `current_rating >= 6`).258- `adverse_loan_count`, `adverse_balance` (2dp) over the rating>=6 set.259- `risk_classes`: asc by `loan_id`; each `{loan_id, risk_class(enum §2.3), factor_score}`.260- `monitoring_cadence` ∈ {monthly, quarterly, semiannual} — pick by worst risk class present261 (Nonaccrual/Projected-Loss → monthly; otherwise quarterly).262263`stress_results`: `shock_label` ("+200bp"/"watch_list_+200bp"), `breach_threshold`=1.0,264`results` asc by `loan_id` **for loans with DSCR only**: `{loan_id, base_dscr(2dp), stressed_dscr(2dp),265breaches_threshold(bool)}` using `stressed_dscr = dscr/1.18`. `breach_loan_ids` asc.266267`workout_queue`: asc/desc by **exposure desc, then loan_id asc**: `{loan_id, exposure(2dp), risk_class,268payment_status(enum), recommended_action(enum §2.7), projected_loss(bool)}`. Includes all adverse loans269(even those without DSCR).270271`severe_bucket_counts`: asc by `(current_rating, payment_status)`; each272`{current_rating, payment_status, loan_count, exposure}` summarizing the adverse population.273274### Task 005 — Harbor competing-CRE275`applications_compared` = the two competing CRE apps (the larger `9xx`-series sentinel apps,276e.g. HAR-APP-901 / HAR-APP-902), asc by `application_id`. Each `{application_id, weighted_cdfi_score(1dp),277score_class(enum), decision(enum), reason_codes(sorted)}`.278279`recommended_path`: `{selected_application_id, path(enum), unselected_application_id,280unselected_disposition ∈ {decline, defer}, unselected_reason_codes(sorted, restricted to281sector_breach/weak_dscr/high_ltv/fdic_adverse_variance)}`. Select = lower weighted score (survives282the dual stress `stressed_dscr = dscr*0.85/1.18 >= 1.0`); the unselected typically breaches stress283and/or sector.284285`stress`: `formula` ("cre_dual_stress"/"stressed_dscr = dscr * 0.85 / (1 + 0.18)"),286`coverage_breach_threshold`=1.0, `results` asc by application_id: `{application_id, base_dscr(2dp),287stressed_dscr(2dp), breaches_threshold(bool)}`.288289`concentration`: `cre_policy_limit_pct`(4dp = branch.cre_policy_limit_pct), `existing_cre_exposure`(2dp290= sum of CRE-sector `current_exposure`; CRE sectors are those whose `limit_pct` == cre_policy_limit_pct),291`existing_cre_concentration`(4dp = exposure/total_loans_outstanding),292`selected_post_approval_cre_concentration`(4dp = (existing + selected requested_amount)/total_loans_outstanding),293`selected_policy_variance_bps`(2dp signed = (post − limit)×10000),294`fdic_benchmark_metric`=`total_real_estate_30_89_pct`, `branch_delinquency_ratio`(4dp, branch analog of that metric — derive from metrics/loans), `fdic_benchmark_ratio`=0.0051(4dp),295`fdic_variance_ratio`(4dp signed = branch − benchmark), `fdic_variance_bps`(2dp signed = ratio×10000).296297`conditions`: subset of the 7 allowed enum values, asc alphabetically (bank_retained_exposure_cap,298committee_cre_exception, updated_appraisal_before_close, tenant_roll_and_lease_review,299minimum_dscr_covenant_1_25, quarterly_financial_reporting, no_additional_cre_without_committee_review).300301---302303## 5. Numeric conventions304305- **Money / USD**: round to **2 decimals**. Sum balances/exposures with 2-dp rounding at the end.306- **Ratios / concentrations / percentages expressed as ratios**: **4 decimals** (e.g. 0.1135).307- **Basis-point variances**: **2 decimals**, **signed** (positive = adverse / over-limit).308 `variance_bps = (branch_ratio − benchmark_ratio) × 10000`.309- **DSCR (base & stressed)**: 2 decimals. **weighted_cdfi_score**: 1 decimal (lower better).310- **Counts, ratings, factor_score**: integers.311- **breach / over_limit / projected_loss**: JSON booleans (`true`/`false`).312- Always honour the precision stated in each template field — it is checked.313314---315316## 6. Anticipated misjudgments & exclusion rules3173181. **Regrade population vs watch-list.** Redwood regrades `current_rating >= 3` (min_current_rating=3);319 Summit watch-list is `current_rating >= 6`. Don't confuse the two thresholds. Within Redwood,320 `target_current_rating_min=3` covers almost the whole book; the *migration* block is the321 `current_rating == 3` subset only.3222. **Severe-delinquency override.** 90+ DPD forces ≥7, Nonaccrual forces 8, even if DSCR is strong.323 Don't let a good DSCR "rescue" a Nonaccrual.3243. **No spurious upgrades.** `final = max(current, factors)` — re-derivation only downgrades/holds.325 Do not upgrade a 5 to 4 because only the 30-DPD floor (4) is available.3264. **Concentration denominator.** Always `total_loans_outstanding` from `/metrics[0]`. Never `total_assets`327 (the branch record's, ~5× larger) — that would shrink every concentration % and hide breaches.3285. **NPA exposure source.** Use `metrics[0].nonperforming_loans` (USD), not a manual count, not total_assets.3296. **Stress-list DSCR filter.** Loans with null DSCR are excluded from `stress_results` but still appear in330 `workout_queue` / `watch_list_summary`. Don't drop them from the portfolio.3317. **`final_rating_exposure_totals` vs `migration_from_current_rating_3`.** Former = all rating>=3 loans by332 final_rating; latter = only current_rating==3 loans by final_rating (and includes `loan_ids`).3338. **Orderings.** `ascending loan_id` is **string** sort ("RED-LN-001" < "RED-LN-011" < "RED-LN-015" <334 "RED-LN-901"). `workout_queue` is exposure-desc then loan_id-asc. `applications_compared`,335 `material_downgrades`, `risk_classes`, `breach_loan_ids`, `severe_bucket_counts`,336 `post_approval_concentrations` — each has its own stated ordering; follow the template literally.3379. **Reason-code lists** sorted ascending alphabetically; `unselected_reason_codes` is restricted to the338 4-value subset, not the full 12.33910. **CRE sectors.** Identify CRE sectors as those whose `/sector-exposures` `limit_pct` equals340 `cre_policy_limit_pct` (0.29), not by name heuristics. Respect `grandfathered` — don't worsen a341 grandfathered over-ceiling sector without a mitigation.34211. **NCUA shape.** Always `.rows[]`; a `?state_code=` call returns a 1-element rows array, not a flat object.34312. **`benchmark_metric` choice.** Match the FDIC metric to the branch/sector mix: general bank NPA review →344 `total_loans_noncurrent_pct`; CRE delinquency variance (Harbor) → `total_real_estate_30_89_pct`;345 construction-heavy → construction metrics. The template enum pins Harbor to `total_real_estate_30_89_pct`.34613. **Two competing apps, not all CRE apps.** Harbor has several CRE-typed applications; the "competing"347 pair is the two large `9xx`-series sentinel apps. Confirm via amounts / the prompt's "two CRE requests".34814. **`score_class` thresholds use `<=`** (approve_quality ≤2.0, conditional ≤3.0, weak >3.0). Lower349 weighted score is better; pick the lower score as selected.35015. **`recent_bankruptcy`** trigger: years_in_business small + `bankruptcy_months_ago` present →351 `recent_bankruptcy`/`startup_risk`. `low_fico` only when fico is non-null and below band.352 `documentation_gap` only when `documentation_complete` is false.353354---355356## 7. Step-by-step derivation SOP (apply per task)3573581. **Read the prompt** → identify branch_id / segment_id, the threshold param (`min_current_rating`),359 and which benchmark (FDIC vs NCUA) applies. Open the answer_template; list every required key, enum,360 and ordering.3612. **Fetch** `/api/policies` once. Fetch `/api/branches/{id}`, `/api/branches/{id}/metrics` (take `[0]`),362 the relevant loans/applications/sector-exposures with the right `?` filter, and the benchmark.3633. **Derive per-field** using §2 rules. Compute factor-derived ratings, stress DSCRs, factors scores,364 concentrations, and variances with the exact formulas and precisions in §5.3654. **Filter & group** with the exact subset rules (regrade pop vs migration subset; DSCR-only stress list;366 adverse rating>=6; decline-only reason map; competing-pair apps).3675. **Sort** each list to the template's ordering (string asc loan_id; exposure-desc; alpha asc reason codes;368 asc by final_rating / sector / trigger_id / application_id).3696. **Round** to the field's precision (money 2dp, ratios 4dp, bps 2dp signed, scores 1dp).3707. **Emit** a single JSON object with exactly the required top-level keys and per-item keys; omit extras.3718. **Self-check** against §6 pitfalls before finishing: denominator? NPA source? override applied? no372 upgrade? enum exact? ordering literal? precision honoured?