diff --git a/API-SPEC.md b/API-SPEC.md
index 7cf77b6..efb501f 100644
--- a/API-SPEC.md
+++ b/API-SPEC.md
@@ -191,6 +191,7 @@ The loan book is the one input no public feed supplies, and no two lenders expos
| `PUT` | `/admin/connectors/profiles/{code}` | mapping and settings; **an invalid mapping is refused here**, not at the next nightly run |
| `POST` | `/admin/connectors/profiles/{code}/test` | reaches the source and shows the columns. **Stages nothing.** |
| `POST` | `/admin/connectors/profiles/{code}/sync` | pulls and stages; writes only if the profile has `autoCommit` |
+| `POST` | `/admin/connectors/profiles/{code}/backfill?from=&to=` | one extract per month across a range, staged as **one** batch |
| `GET` | `/admin/connectors/runs` | every attempt, successful or not |
| `GET` | `/admin/connectors/runs/{runId}/preview` | the staged batch behind a run |
@@ -200,6 +201,23 @@ The loan book is the one input no public feed supplies, and no two lenders expos
**Auto-commit is off by default.** A sync stages and stops, preserving the manual path's rule that nothing is written until someone has seen what will be. A bank that wants the nightly feed unattended turns it on per profile, knowingly.
+### DPD history — the training label
+
+| Method | Path | Notes |
+|---|---|---|
+| `GET` | `/admin/loan-history/coverage` | three buckets and a verdict in words |
+| `GET` | `/admin/loan-history/series/{accountRef}` | one account's snapshots, oldest first |
+| `POST` | `/admin/loan-history/generate?months=24` | synthetic monthly snapshots, stamped `SEEDED_HISTORY` |
+| `DELETE` | `/admin/loan-history/generated` | removes generated rows only |
+
+**Every `loan_record` carries a `source`:** `SEEDED`, `SEEDED_HISTORY`, `UPLOAD`, or the connector profile code. Coverage reports **three** buckets — synthetic, real, and unknown provenance — and never folds unknown into real. A row whose origin is unlabelled is not evidence of a lender extract, and treating it as one is exactly the mistake the column exists to prevent.
+
+**Generated history is shaped like a loan book, not like noise.** Real DPD steps: an account either pays and cures to zero, or misses and rolls forward one instalment period. So the miss/pay sequence is drawn first and DPD derived from it (`DpdWalk`), and the day-of-month slack is **fixed per delinquency episode** — a due date does not move between two missed payments, so an account 5 days down that misses again is 35 days down, not 12. Drawing the slack per month made consecutive months differ by 1 to 59 days, which is not a roll-forward and would have taught a model a transition matrix no book produces.
+
+The series is walked **backwards from the state already on the book**, and the trailing episode's slack is derived from the anchor's own DPD, so the generated history joins today's number exactly. Generation never overwrites an existing row: a real extract survives a regeneration untouched.
+
+The simulated adapters walk the same `DpdWalk`, so a connector backfill returns a real-shaped series rather than today's DPD repeated for every month.
+
A failed run is still a run. Nothing to read is `SUCCESS` with a message, not `FAILED` — a bank that has not delivered tonight's file is a normal state, and conflating it with a broken connector trains everyone to ignore the status.
---
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 0bea7a3..529bd87 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -218,6 +218,7 @@ believes.
| Risk engine + rule engine + admin API | ✅ Phase D — see §6.3 |
| Console wire-up: rules screen, market panel, freshness | ✅ Phase E — see §6.4 |
| Core banking / LMS connector seam | ✅ Phase F — see §6.5 |
+| DPD history: generated + connector backfill | ✅ Phase F — see §6.6 |
| LightGBM quantile heads + SHAP | ○ R1 milestone |
Phases B, C and D each terminate in a table you can inspect, so each is testable
@@ -379,6 +380,41 @@ That fix belongs to the manual upload path as much as to the connector.
synced by something the bank already runs), scheduled execution (`scheduleCron` is
recorded and read by nothing), and incremental watermarks.
+### 6.6 DPD history — the training label, and where it comes from
+
+Package `com.lincfusion.udyamlinc.loanhistory`, plus a fourth tab on
+`/officer/integration`.
+
+The book held **one snapshot per loan**, so there was no history at all and
+nothing a model could be trained against. Two routes now fill it, and the
+difference between them is stated rather than implied.
+
+| Route | What it is |
+|---|---|
+| `LoanHistoryService.generate` | Synthetic monthly snapshots, every row stamped `SEEDED_HISTORY`. Exercises the feature store, the point-in-time joins and the label plumbing before a bank has delivered anything. |
+| `ConnectorSyncService.backfill` | One extract per month across a range, mapped and staged as a single batch. This is the real path: every core can answer "what did the book look like at each month end last year". |
+
+**Provenance is the whole design.** `LoanRecord.source` is stamped at every write
+site, and coverage reports **three** buckets — synthetic, real, unknown — never
+folding unknown into real. Generation refuses to overwrite an existing row, so a
+lender's extract survives a regeneration.
+
+**The generator's arithmetic had to be got right, and was not at first.** Real DPD
+steps: pay and cure to zero, or miss and roll forward one instalment period. The
+first version added day-of-month slack **per month**, so consecutive months
+differed by anything from 1 to 59 days — a shape no loan book produces, and one
+that would have taught a model a nonsense transition matrix. The slack is now
+fixed per delinquency episode (a due date does not move between two missed
+payments), and for the run reaching the anchor it is derived from the anchor's own
+DPD so the series joins today's number exactly. Verified against a running
+instance: every generated series now rolls by exactly 30, and the only steps that
+are not are the joins to pre-existing rows the generator correctly refused to
+touch.
+
+**What this does not claim.** With no lender extract on the book, coverage reports
+`trainable: false` and says so in words. Training on generated history and
+reporting the accuracy would be measuring the generator.
+
---
## 7. Explicitly not being built yet
diff --git a/src/main/java/com/lincfusion/udyamlinc/connector/ConnectorController.java b/src/main/java/com/lincfusion/udyamlinc/connector/ConnectorController.java
index 7b9b510..5d341bc 100644
--- a/src/main/java/com/lincfusion/udyamlinc/connector/ConnectorController.java
+++ b/src/main/java/com/lincfusion/udyamlinc/connector/ConnectorController.java
@@ -185,6 +185,34 @@ public class ConnectorController {
return ApiResponse.ok(result.run().getMessage(), body);
}
+ /**
+ * Pulls one extract per month across a range and stages them as one batch.
+ *
+ *
This is how DPD history arrives from a lender that has it. Every core can
+ * answer "what did the book look like at each month end last year" — it is the
+ * same query behind their regulatory returns — so the honest way to get the
+ * training label is to ask once, rather than wait a year for a nightly sync to
+ * accumulate it.
+ */
+ @PostMapping("/profiles/{code}/backfill")
+ public ApiResponse