PortfolioPlan makes the demo book explicit — 263 enterprises across six states,
one per NABARD zone, over eight partner banks and sixteen branches — and
PortfolioPurge is the destructive escape hatch for the one boot where that shape
changes. The seeder never rewrites an existing enterprise, so a plan change
without a purge leaves orphans rather than corrupting rows.
udyam.forecast.trust-seeded now defaults to false. It was true only because
ledger coverage reached six enterprises of 142, so without the fallback the
console had almost no forecasts and showed 34 trip-wires of which 5 were real.
The seeder now writes five months of ledger for every enterprise, band-coherent
— a RED enterprise's monthly net genuinely sits below its instalment — so the
computed forecast is the one worth showing and the fallback is no longer
load-bearing.
jwt.secret becomes UDYAM_JWT_SECRET-overridable. The default stays committed so
a fresh clone boots without setup, which is precisely why it is not a secret:
any deployment holding real borrower data must set it.
Two traps this run surfaced, both silent:
- dealBands() now deals round-robin across sectors. Appending HORTICULTURE to
the sector list put it last and the fixed-seed band shuffle was drained by
the five packs ahead of it, so all 19 seeded STANDARD.
- Anything added to the seeder after a database was first populated never
reaches it, because of the count() > 0 guards. backfillPortfolio() now
covers loans and ledger and FeatureCatalog.seed() reconciles tolerances, but
trip-wire rules and sector weights still carry the hazard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
195 lines
10 KiB
Properties
195 lines
10 KiB
Properties
spring.application.name=udyamlinc-backend
|
||
|
||
server.port=8082
|
||
server.servlet.context-path=/api/v1/udyam
|
||
|
||
# JPA & Hibernate
|
||
spring.jpa.hibernate.ddl-auto=update
|
||
spring.jpa.properties.hibernate.default_schema=public
|
||
spring.jpa.open-in-view=false
|
||
|
||
# DataSource — UdyamLinc_DEMO
|
||
spring.datasource.url=${UDYAM_DB_URL:jdbc:postgresql://localhost:5432/UdyamLinc_DEMO}
|
||
spring.datasource.username=${UDYAM_DB_USERNAME:postgres}
|
||
spring.datasource.password=${UDYAM_DB_PASSWORD:root}
|
||
spring.datasource.driver-class-name=org.postgresql.Driver
|
||
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
|
||
|
||
# HikariCP
|
||
spring.datasource.hikari.connection-timeout=10000
|
||
spring.datasource.hikari.minimum-idle=5
|
||
spring.datasource.hikari.maximum-pool-size=20
|
||
spring.datasource.hikari.idle-timeout=600000
|
||
spring.datasource.hikari.max-lifetime=1800000
|
||
spring.datasource.hikari.auto-commit=true
|
||
|
||
# throw NoHandlerFoundException for 404s
|
||
spring.mvc.throw-exception-if-no-handler-found=true
|
||
|
||
# Demo signing key, overridable by environment. The default is committed on
|
||
# purpose so a fresh clone boots without setup — which also means it is public,
|
||
# and therefore not a secret. Any deployment carrying real borrower data must set
|
||
# UDYAM_JWT_SECRET; a key readable from the repository signs anyone's token.
|
||
jwt.secret=${UDYAM_JWT_SECRET:55647961_6D4C_696E_6320_4E41_4241_5244_5F32_3032_365F_4C69_6E63_4675_7369_6F6E_5F48_6163_6B61_74686F6E}
|
||
# 24 hours
|
||
jwt.expiration=86400000
|
||
|
||
# The Vite dev server for UdyamLinc-UI (5180 is BharatLinc)
|
||
udyam.cors.allowed-origins=http://localhost:5182,http://127.0.0.1:5182,http://localhost:4173
|
||
|
||
# Forecast horizon in months — the deck's 3–6 month early-warning window
|
||
udyam.forecast.horizon-months=6
|
||
|
||
# Composite Stress Score band thresholds; map these to the institution's watchlist
|
||
udyam.risk.red-threshold=65
|
||
udyam.risk.watch-threshold=35
|
||
|
||
# ── Aggregate suppression ───────────────────────────────────
|
||
# Fewest borrowers in a cell before its numbers may be published. Below this the
|
||
# cell reports "suppressed" rather than a rounded figure, because a district of
|
||
# two borrowers and one alert names that borrower as surely as printing them.
|
||
udyam.apex.min-cell-size=5
|
||
# The same threshold for a Regional Office session, separated so it can be tuned
|
||
# without touching the national one.
|
||
#
|
||
# It defaults to the SAME value on purpose. Suppression protects against
|
||
# re-identifying a borrower from an aggregate, and that risk turns on what the
|
||
# reader already knows — a regional officer, supervising four districts and on
|
||
# first-name terms with the banks in them, can re-identify from a cell of three
|
||
# far more easily than someone reading a national table. So the argument from
|
||
# proximity runs the other way: if these two ever diverge, the regional figure
|
||
# has a better case for being HIGHER than the national one, not lower.
|
||
#
|
||
# Lower it only if a supervisor has decided that trade-off deliberately, and
|
||
# knows that a small office's sector cells are where it will show.
|
||
udyam.apex.min-cell-size.regional-office=5
|
||
|
||
# ── Risk & rule engine (ARCHITECTURE Phase D) ───────────────
|
||
# Which ScoringStrategy binds. RULE today; MODEL when the LightGBM heads land —
|
||
# that swap is this line, not a rewrite.
|
||
udyam.risk.engine=RULE
|
||
udyam.risk.model-version=rule-v1.0.0
|
||
# Where a family lands with no usable feature. Neutral, not innocent: scoring 0
|
||
# would read as "safe on this dimension" when the truth is that we do not know.
|
||
udyam.risk.family-prior=40
|
||
# A rule engine reports low confidence by design.
|
||
udyam.risk.rule-confidence-ceiling=70
|
||
# Nightly scoring, an hour behind ingestion so it scores what that job wrote.
|
||
udyam.risk.scheduled=${UDYAM_SCORING_ENABLED:false}
|
||
udyam.risk.cron=0 0 3 * * *
|
||
|
||
# Seed the demo portfolio on first boot: six states, one per NABARD zone, each
|
||
# carrying 18 enterprises across the five sector packs (108 in all).
|
||
udyam.seed.enabled=true
|
||
|
||
# Destructive. When true, the seeder deletes any enterprise not in PortfolioPlan
|
||
# along with its runs, alerts, visits, ledger and loan rows. Needed for exactly
|
||
# one boot when the portfolio's shape changes — the move from 142 Madhya Pradesh
|
||
# enterprises to 108 across six states is such a change. Turn it off again after.
|
||
udyam.seed.reshape=${UDYAM_SEED_RESHAPE:false}
|
||
|
||
# ── Ingestion (ARCHITECTURE Phase B) ────────────────────────
|
||
# Off by default: a demo boot must never call a government endpoint. Turn on for
|
||
# the pilot, or trigger a one-off pass with POST /admin/ingestion/run.
|
||
udyam.ingestion.enabled=${UDYAM_INGESTION_ENABLED:false}
|
||
udyam.ingestion.cron=0 0 2 * * *
|
||
udyam.ingestion.zone=Asia/Kolkata
|
||
udyam.ingestion.http.connect-timeout-ms=10000
|
||
udyam.ingestion.http.read-timeout-ms=30000
|
||
|
||
# Agmarknet mandi prices — data.gov.in Open Government Data API.
|
||
# Register at https://data.gov.in for a key; without one the run records SKIPPED.
|
||
udyam.ingestion.datagov.api-key=${DATAGOV_API_KEY:}
|
||
udyam.ingestion.datagov.base-url=https://api.data.gov.in/resource
|
||
# "Variety-wise Daily Market Prices Data of Commodity" — the historical Agmarknet
|
||
# resource, ~81M rows, current to yesterday. NOT 9ef84268-…, which is "Current
|
||
# Daily Price": a today-only snapshot carrying a single arrival_date, from which
|
||
# no week-over-week history can be built and in which none of the four pilot
|
||
# districts appear.
|
||
udyam.ingestion.datagov.resource-id=${DATAGOV_RESOURCE_ID:35985678-0d79-46b4-9ed6-6f13308a1d24}
|
||
udyam.ingestion.datagov.page-limit=1000
|
||
# Page ceiling per (district, commodity). Only reached when a run asks for a deep
|
||
# lookback — POST /admin/ingestion/run/AGMARKNET?lookbackWeeks=156 is the one-off
|
||
# three-year backfill that gives price_z_score its seasonal mean. 40 pages of
|
||
# 1000 rows is ample for three years of one district's arrivals.
|
||
udyam.ingestion.datagov.max-pages=40
|
||
# Filter names must match the resource's own field names; they differ by resource
|
||
# version. This resource capitalises them, and — unlike the current-price
|
||
# resource — matches exactly on the bare name. Appending ".keyword" here returns
|
||
# zero rows; omitting the capital returns rows from every state whose name shares
|
||
# a token, so "Madhya Pradesh" would silently pull Andhra, Himachal and Uttar.
|
||
udyam.ingestion.datagov.field.state=State
|
||
udyam.ingestion.datagov.field.district=District
|
||
udyam.ingestion.datagov.field.commodity=Commodity
|
||
# Rows come back in arbitrary order and the lookback filter is applied client
|
||
# side, so without a descending sort a 1000-row page is mostly years-old data and
|
||
# every commodity reports zero weeks written.
|
||
udyam.ingestion.datagov.sort-field=Arrival_Date
|
||
# data.gov.in meters by quota per window, so the limit is reached by the run and
|
||
# not by any one call — once it trips, every remaining pair in that run fails
|
||
# together and the whole ingestion records zero rows. Verified 2026-08-11: a
|
||
# 62-pair run straight after a 62-pair probe returned 429 on all 62. The backoff
|
||
# is long because the window is measured in minutes, not seconds.
|
||
udyam.ingestion.datagov.retry-attempts=3
|
||
udyam.ingestion.datagov.retry-backoff-ms=45000
|
||
udyam.ingestion.mandi.lookback-weeks=12
|
||
|
||
# Climate — Open-Meteo reanalysis archive. No key. The archive trails real time,
|
||
# hence the lag; the baseline is fetched once per district and cached as normals.
|
||
udyam.ingestion.climate.base-url=https://archive-api.open-meteo.com/v1/archive
|
||
udyam.ingestion.climate.timezone=Asia/Kolkata
|
||
udyam.ingestion.climate.lookback-weeks=12
|
||
udyam.ingestion.climate.lag-days=5
|
||
udyam.ingestion.climate.heat-threshold-c=40
|
||
udyam.ingestion.climate.baseline-from=1995-01-01
|
||
udyam.ingestion.climate.baseline-to=2024-12-31
|
||
# Open-Meteo meters by request cost, so the 30-year baseline call alone can trip
|
||
# the minutely limit and 429 the district calls queued behind it. A rate-limited
|
||
# call waits and retries; anything else fails once and is reported.
|
||
udyam.ingestion.climate.retry-attempts=3
|
||
udyam.ingestion.climate.retry-backoff-ms=20000
|
||
|
||
# ── Feature store (ARCHITECTURE Phase C) ────────────────────
|
||
# Window sizes for the derivations. A level is not a feature; every one of these
|
||
# turns a level into a change, a ratio or an anomaly against one of these windows.
|
||
udyam.features.price-window-weeks=8
|
||
udyam.features.recent-window-weeks=4
|
||
udyam.features.climate-window-weeks=8
|
||
# Below this many same-season observations a z-score is noise, so it stays null.
|
||
udyam.features.min-zscore-samples=6
|
||
|
||
logging.level.com.lincfusion.udyamlinc=INFO
|
||
|
||
# ── Forecast (ARCHITECTURE R1 seam) ─────────────────────────
|
||
# DETERMINISTIC until the LightGBM quantile heads exist. Both implement
|
||
# ForecastStrategy, so the swap is this one property.
|
||
udyam.forecast.engine=${UDYAM_FORECAST_ENGINE:DETERMINISTIC}
|
||
udyam.forecast.lookback-months=6
|
||
udyam.forecast.min-months=2
|
||
# P10/P90 half-width at h=1 when every input is present; each missing input adds
|
||
# to it, and it grows with the square root of the horizon. Missing inputs widen
|
||
# the band, never lower the centre — a pessimistic P50 built on ignorance would
|
||
# manufacture breaches.
|
||
udyam.forecast.base-spread=0.18
|
||
udyam.forecast.missing-input-penalty=0.10
|
||
udyam.forecast.max-spread=0.75
|
||
udyam.forecast.max-drift=0.30
|
||
# Whether dscr_p10 may fall back to the seeder's forecast rows, which carry no
|
||
# engine. True keeps the demo portfolio populated; false makes every enterprise
|
||
# the forecast cannot reach report NO_FORECAST, and TW-01 skip rather than fire
|
||
# on a number nothing computed. Set false before any run whose alerts are meant
|
||
# to be believed.
|
||
# Now false. It was true only because ledger coverage reached six enterprises of
|
||
# 142, so without the fallback the console had almost no forecasts to show and 34
|
||
# trip-wires of which 5 were real. The seeder now writes five months of ledger for
|
||
# every enterprise, band-coherent — a RED enterprise's monthly net genuinely sits
|
||
# below its instalment — so the computed forecast is the one worth showing.
|
||
udyam.forecast.trust-seeded=${UDYAM_TRUST_SEEDED_FORECASTS:false}
|
||
|
||
# Furthest horizon TW-01 may act on. The six-month fan is still forecast, stored
|
||
# and drawn; this bounds only what the trip-wire fires from. Past month three the
|
||
# band is dominated by compounding uncertainty rather than by anything specific
|
||
# to the enterprise — at the 0.75 ceiling P10 is a quarter of P50 for everyone,
|
||
# so a worst-of-six rule separates nobody from anybody.
|
||
udyam.forecast.tripwire-horizon=3
|