# Reference Estate: Tidewater's Legacy Systems

> **Status:** Draft 0.1, 2026-11-05. Every program, dataset, number, and incident here is invented. Tidewater, its partner banks, its cast, its domains, and Anchor Core come from Gravity Standard's [reference application](https://gravitystandard.com/). This document adds the history behind them and doesn't change anything the Standard says.

## 1. The story

Tidewater Lending Services has serviced small-business loans for partner banks since the 1980s. Before it adopted Gravity Standard, nearly everything ran on three systems:

| System | Platform | What it does | Age |
|---|---|---|---|
| **Anchor Core** | z/OS: COBOL, CICS, Db2, VSAM, IBM MQ, RACF, JCL | Ledger, balances, payment posting, nightly batch | Since 1987 |
| **BankLink** | Java on WebSphere, reading Anchor Core's Db2 | Partner bank portal: payoff quotes and file uploads | Since 2004 |
| **KeelTrack** | IBM i: RPG, CL, DDS files, 5250 screens | Collateral insurance tracking, inherited when Tidewater bought Keel Street Servicing | Since 1996, acquired 2009 |

The Standard says Anchor Core remains the system of record, and its ledger is ledger-class data (SCOPE-001). So this is the migration of everything *around* the ledger, which is where most of the business's workflow is trapped.

## 2. Dispositions

| Capability | Today | Disposition | Reason |
|---|---|---|---|
| Ledger and balances | Anchor Core | **Retain** | Ledger-class data (SCOPE-001) |
| Late payment follow-up | `ACLP210` nightly job | **Move** | Workflow; the Standard's Payments application specifies it |
| Remittance intake | `TWRMT100`, Connect:Direct | **Move** | File-driven workflow |
| Funding notices | MQ into Anchor Core | **Move** | Reconciliation workflow |
| Payment correction | CICS transaction `TWPY` | **Move** | A human review belongs in a process |
| Payoff quotes | BankLink | **Move** | A front-door request; reads its balance from Anchor Core |
| Collateral insurance tracking | KeelTrack | **Replace** | A commodity service; a product does this better |
| Monthly statements | `TWSTMT` | **Move, once Anchor Core supplies a balance extract** | Statements show balances, which stay on Anchor Core (SCOPE-001) |
| Past-due print report | `TWRPT44` | **Retire** | Nobody has read it in 30 months |

### Records and ledgers

| Data | Class | Lives in |
|---|---|---|
| Loan identity, terms, and status | Authoritative record | The Standard, as it already says |
| Scheduled payment status, funding notices | Authoritative record | The Standard, once moved |
| Late fee calculation | Authoritative record | Payments calculates it and hands it to Anchor Core |
| Current balance | Ledger-class | Anchor Core; Loan holds only "as last reported" |
| Accrued interest, general ledger | Ledger-class | Anchor Core |

Anchor Core can't be switched off until its ledger has a home. Tidewater's next step there is a separate decision about a core banking product.

## 3. The translations

### 3.1 COBOL to rules: the late fee

`ACLP210` paragraph `2200-ASSESS-FEE`:

```cobol
       2200-ASSESS-FEE.
           IF PMT-BANK-ID = '0417'
              MOVE ZERO TO WS-LATE-FEE
           ELSE
              COMPUTE WS-LATE-FEE = PMT-AMT-DUE * 0.05
              IF WS-LATE-FEE < 10.00
                 MOVE 10.00 TO WS-LATE-FEE
              END-IF
           END-IF.
```

- `COMPUTE` without `ROUNDED` truncates. On $1,234.70 the fee is $61.73, not $61.74.
- Bank `0417` is Granite Valley, waived by a 1998 agreement nobody had written down elsewhere.

Becomes:

- **LPF-003:** A late fee is 5% of the amount due, with any fraction of a cent dropped, and is never less than $10.00.
- **LPF-004:** A bank with the fee waived parameter set is never charged a late fee. (Granite Valley's value is tenant configuration.)

### 3.2 RPG to rules: the lapse check

`KTLAPSE` in KeelTrack:

```rpgle
chain (loanNo) COLLATPF;
if %found(COLLATPF) and CVGEXP < today - %days(30);
   LAPSEFLG = 'Y';
   update COLLATR;
endif;
```

KeelTrack is being replaced, but it still gets a rule, because the rule is the product's acceptance test (MIG-017):

- **CIN-001:** A loan's collateral counts as uninsured once its coverage has been expired for more than 30 days.

### 3.3 Java to rules and contracts: the payoff quote

BankLink's `QuoteService.java` and a properties file on the application server:

```java
BigDecimal perDiem = balance.multiply(rate).divide(DAYS_360, 2, RoundingMode.HALF_UP);
Date goodThrough = addBusinessDays(today, config.getInt("quote.valid.days"));
```

```properties
quote.valid.days=10
```

The code's repository doesn't contain the 10; only the server does. Becomes:

- **PQ-001:** A payoff quote is good through 10 business days after the day it is issued.
- **PQ-002:** A payoff quote's daily interest is the balance times the annual rate divided by 360, rounded half-up to the cent.

### 3.4 Flat files and VSAM to record types: Scheduled Payment

```cobol
       01  PMT-DUE-REC.
           05  PMT-BANK-ID      PIC X(04).
           05  PMT-LOAN-NO      PIC X(12).
           05  PMT-LOAN-CLASS   PIC X(01).
           05  PMT-DUE-DATE     PIC 9(08).
           05  PMT-AMT-DUE      PIC S9(7)V99 COMP-3.
           05  PMT-STATUS       PIC X(01).
           05  PMT-REF-NO       PIC X(10).
           05  PMT-ACH-TRACE    REDEFINES PMT-REF-NO PIC X(10).
```

| Source | Becomes | Note |
|---|---|---|
| `PMT-BANK-ID` | The tenant | Not a field; the platform carries it (MIG-012) |
| `PMT-LOAN-NO` | loan number: `key of borrowing.loan`, internal | A business key, as DATA-028 requires |
| `PMT-LOAN-CLASS` | loan class on Loan: `choice(standard, guaranteed)`, internal | Belongs to the loan, not the payment |
| `PMT-DUE-DATE` | due date: `date`, internal | `99999999` meant "deferred" and becomes the `deferred` question below |
| `PMT-AMT-DUE` | amount: `money`, confidential | The source never protected it |
| `PMT-STATUS` | status: `expected`, `received`, `late`, `cancelled` | `E`, `R`, `L`, `C` |
| `PMT-REF-NO` / `PMT-ACH-TRACE` | Dropped here; check reference and ACH trace belong on Payment | One field, two meanings (MIG-010) |

Open question: `99999999` appears on 1,204 records. The Standard's statuses have no `deferred`. Is it a new status, or a Scheduled Payment that shouldn't exist yet? The Payments steward decides.

### 3.5 Db2 to domains: Loan

```sql
CREATE TABLE TWDB.LOAN (
  BANK_ID   CHAR(4)  NOT NULL,
  LOAN_NO   CHAR(12) NOT NULL,
  LOAN_STAT CHAR(1)  NOT NULL,  -- A W C X
  ...
```

- **Value profile:** the comment lists four statuses. Production has five. `H` appears on 41 loans, set by hand during a 2003 dispute. A person decides what those loans are.
- **Stored procedure:** `SP_CLOSE_LOAN` refuses to close a loan with a balance. That's a rule nothing else states. **LON-001:** A loan closes only when its reported balance is zero.
- **Sort order:** EBCDIC sorts letters before digits, and ASCII sorts digits first. Harborside's loan numbers mix both, so its portfolio list changes order unless a rule states the order. **LON-002:** Loan lists sort by loan number with letters before digits.

### 3.6 IBM MQ to the integration list

| Queue | What it really does | Becomes |
|---|---|---|
| `TW.FUNDING.NOTICE.IN` | A clearing service sends funding notices. The consumer assumes the remittance file arrived first, and parks the rest for manual work: 3% of notices | SQS with a DLQ on the source queue. Reconciliation already allows either order, so the parking pile disappears |
| `TW.LOAN.STATUS.PUB` | Publishes the whole loan record, balance included, to BankLink, KeelTrack, and the warehouse | The outbox relayed to EventBridge; events carry identifiers, not data |
| `TW.DEAD` | The dead-letter queue. It holds 2,214 messages, some from 2019, and nobody watches it | A DLQ with an alarm, an owner, and a redrive runbook. Each old message gets an answer before cutover |

### 3.7 JCL and batch to processes

```jcl
//ACLP210  JOB (TW),'LATE SWEEP',CLASS=B
//STEP010  EXEC PGM=SORT
//SORTIN   DD DSN=TW.PROD.PMTDUE(0),DISP=SHR
//SORTOUT  DD DSN=&&PMTSRT,DISP=(NEW,PASS)
//SYSIN    DD *
  SORT FIELDS=(1,4,CH,A,18,8,CH,A)
  INCLUDE COND=(17,1,CH,NE,C'G')
/*
//STEP020  EXEC PGM=ACLP210,COND=(4,LT,STEP010)
//PMTIN    DD DSN=&&PMTSRT,DISP=(OLD,DELETE)
//FEEOUT   DD DSN=TW.PROD.LATEFEE(+1),DISP=(NEW,CATLG,DELETE)
```

- **The rule in the sort card:** `INCLUDE COND` removes class `G` before the program runs. **LPF-005:** A government-guaranteed loan never enters late payment follow-up. The COBOL never mentions it.
- **Purpose and deadline:** the job finds late payments by morning. A nightly sweep existed because a batch window was the only scheduling tool. The Standard's answer is a process per scheduled payment, with a timer (DATA-021).
- **Ordering:** the scheduler runs `ACLP210` after `TWPOST` and after `TWBKUP`. Posting is a data dependency, because a payment posted tonight isn't late. The backup is not, so it's dropped (MIG-019).
- **True bulk:** `TWSTMT` writes every borrower's statement by the fifth business day. That really is bulk, so it becomes the scheduled process in section 3.16. It moves once Anchor Core supplies a period-end balance extract through quarantine.
- **IBM i dialect:** KeelTrack's `KTNIGHT` CL program submits `KTLAPSE` with `SBMJOB` to a job queue from a `WRKJOBSCDE` entry. Its operators watch it on `WRKACTJOB` and restart it by hand, so their restart note becomes an idempotent stage (MIG-020).

### 3.8 RACF and object authority to access rules

```
PERMIT 'TW.PROD.PMTDUE.**' CLASS(DATASET) ID(LATEOPS) ACCESS(UPDATE)
PERMIT 'TW.PROD.PMTDUE.**' CLASS(DATASET) ID(TWAUDIT) ACCESS(READ)
```

```
GRTOBJAUT OBJ(KTLIB/COLLATPF) OBJTYPE(*FILE) USER(*PUBLIC) AUT(*CHANGE)
```

| Finding | Decision |
|---|---|
| `LATEOPS` reaches every bank's records | There was no bank boundary. Marcus Bell works across banks by design. Sam Whitfield gets Granite Valley only, enforced by Verified Permissions and RLS |
| BankLink connects as one technical ID, `TWWEB` | Real access rules were inside the Java. Each one is extracted as evidence |
| `*PUBLIC *CHANGE` on KeelTrack's `COLLATPF` | Nobody decided that. Not carried forward (MIG-024) |
| Sixteen IDs in `LATEOPS`, four with activity | Twelve grants aren't carried forward |

#### Outside systems

Each outside system proves itself through a registered provider (MIG-041).

| Caller | Today | Provider | How it gets in |
|---|---|---|---|
| Clearing service, funding notices | An MQ channel into Anchor Core | `clearing-svc` (OIDC) | Assumes a role per bank, with each bank's grant |
| Harborside's file exchange | Connect:Direct | `harborside-entra` (OIDC) | Presigned upload to quarantine, for Harborside only |
| Pinecrest's staff | BankLink, as `TWWEB` | Not a system | People sign in |
| Tidewater calling a partner | Case by case | The partner's choice | Stated in `## Depends on`, with a `DESIGN-nnn` |

### 3.9 CICS and 3270 to tasks: payment correction

Transaction `TWPY` moves a misapplied payment. An operator types the loan, payment, and target loan, presses PF5, and it updates in one unit of work.

- **Task:** the Standard's Payment Correction process: request, review (human), apply, notify, close.
- **What the screen hid:** the terminal had no second reviewer. The new process adds one, and that decision is recorded. **PCR-002:** The person who reviews a correction is not the person who requested it.
- **Unit of work:** `TWPY` updates the payment and the suspense entry before `SYNCPOINT`. **PCR-001:** A corrected payment and its suspense entry change together, or neither changes.
- **Speed:** Marcus does a `TWPY` entry in 14 seconds, without a mouse. That's the baseline the new request form is accepted against (MIG-026).
- **IBM i dialect:** KeelTrack's 5250 inquiry screen disappears with the product, but its F4 lookup is on the product's acceptance list, because staff depend on it.

### 3.10 Web interfaces: the rule only the browser knew

```javascript
if (payoffAmount > 250000 && !secondApprover) { showError("Second approver required"); }
```

BankLink's bulk upload path never ran that script. Six production payoffs skipped it. Becomes **PQ-003:** A payoff above the bank's second-approver limit needs a second person's approval, whichever way it was requested. The limit is a tenant parameter.

### 3.11 Partner files: remittances

- **Harborside** sends Standard Remittance files by Connect:Direct. **Pinecrest** uploads hand-prepared fixed-width files through BankLink.
- `TWRMT100` writes straight to VSAM. It also silently drops Pinecrest rows with a zero amount, and no bank has ever been told.
- **Becomes:** Harborside's system signs in through its OIDC provider and uploads to quarantine, and Pinecrest's staff sign in and upload by hand. Then `ingest-standard-remittance` or `ingest-pinecrest-legacy`. Rows with errors go on the row-level error report, so the silent repair becomes visible (a fixed defect).
- The file trailer's control total becomes **RMT-001:** A file's rows must add up to the total its trailer states.

### 3.12 Reports and print

| Report | Usage evidence | Becomes |
|---|---|---|
| `TWRPT44` past-due listing, 132 columns | The spool entry was purged unread for 30 months | Retired |
| `TWRPT09` remittance exceptions | Sam acts on it every month | The exception review task; the report is no longer needed |
| `TWRPT61` regulator portfolio summary | Filed quarterly | Tidewater's choice: a scheduled process loads its reporting system, and the summary is built there |

### 3.13 Code tables and utilities

- `TWCODES`, a shared VSAM table, holds grace days by bank, with keys such as `GRACE0417`. These become the tenant parameter **grace days**.
- It also holds 212 other codes. Usage evidence shows 38 are read. Those become `choice(...)` values or parameters, and the rest are dropped with a reason (MIG-009).
- `IDCAMS REPRO` steps that back up files have no business meaning. They're recorded and dropped.

### 3.14 Postings to a retained system: Post Late Fee

Late Payment Follow-up doesn't post its fee itself. It starts Post Late Fee, because what happens when Anchor Core refuses or goes quiet is a business decision.

```markdown
# Post Late Fee
Code: PLF
Application: payments

## Stages
1. Post fee (automated, posts to Anchor Core)
2. Wait for confirmation (automated)
3. Check with Anchor Core (automated)
4. Review (human, skill: reconciliation)
5. Record outcome (automated)

## Flow
| From                   | When                      | To                     |
|------------------------|---------------------------|------------------------|
| Post fee               | sent                      | Wait for confirmation  |
| Wait for confirmation  | accepted                  | Record outcome         |
| Wait for confirmation  | refused: not enough funds | Record outcome         |
| Wait for confirmation  | refused: any other reason | Review                 |
| Check with Anchor Core | found                     | Record outcome         |
| Check with Anchor Core | not found or no answer    | Review                 |
| Review                 | resolved                  | Record outcome         |

## Timeouts
| Stage                  | Overall | If not done                       |
|------------------------|---------|-----------------------------------|
| Wait for confirmation  | 1 day   | continue at Check with Anchor Core |
| Check with Anchor Core | 4 hours | continue at Review                |
| Review                 | 3 days  | abort                             |

## Rules
Direct
- PLF-001: A fee on an account without enough funds is recorded as owed, not dropped.
- PLF-002: A fee refused for any other reason goes to a person, with the reason.

Indirect
- PLF-003: A fee doesn't show as posted until Anchor Core confirms it.
- PLF-004: A fee with no confirmation after one day is checked before anything else happens.
- PLF-005: A fee is never posted twice.

## If aborted
The fee stays unposted, and the bank's late follow-up shows that it needs attention.
```

The posting goes by MQ into the nightly `TWPOST` run, so confirmation arrives in the next morning's file. That file's receiver resumes the waiting process. Anchor Core can't recognize a duplicate, so its timeout never resends. It goes to Check instead, which needs a small new inquiry on Anchor Core. A later reversal is its own process, and daily reconciliation of fee counts and totals per bank is another.

### 3.15 Archives and audit

Anchor Core keeps seven years of `TW.PROD.LATEFEE` generations on virtual tape. Decommissioning a slice doesn't end that obligation, so the archive needs a readable format and a named owner before anything is switched off (MIG-037).

### 3.16 True bulk to a scheduled process: Monthly Statements

A schedule starts the process daily, and its first stage checks the bank calendar. Each statement is a PDF, built by a small piece of custom code that the process runs, and published to the bank's documents. Handing output to a print vendor would be a hand-off, with its own rules.

```markdown
# Monthly Statements
Code: STM
Application: payments

## Stages
1. Check the calendar (automated)
2. List the loans (automated)
3. Produce statements (sub-process, in chunks)
4. Review failures (human, skill: reconciliation)
5. Close the run (automated)

## Who can start it
A schedule: every day at 6:00 a.m. Central.

## Flow
| From               | When                       | To               |
|--------------------|----------------------------|------------------|
| Check the calendar | not the fifth business day | The request ends |
| Check the calendar | it is                      | List the loans   |
| Produce statements | any failed                 | Review failures  |
| Produce statements | none failed                | Close the run    |
| Review failures    | resolved                   | Close the run    |

## Timeouts
| Stage              | Overall | If not done                 |
|--------------------|---------|-----------------------------|
| Produce statements | 2 days  | continue at Review failures |
| Review failures    | 2 days  | abort                       |

## Volume
| Items      | Expected per run | Decrypts per item | At once |
|------------|------------------|-------------------|---------|
| statements | 19,000           | 1                 | 50      |

## Rules
Direct
- STM-001: Every active loan gets one statement for each month, by the fifth business day.
- STM-002: A loan that closed during the month gets a final statement.

Indirect
- STM-003: A statement that keeps failing goes to a person, and the rest of the run continues.
- STM-004: If more than 5% of statements fail, the run stops and goes to a person.
- STM-005: No borrower gets two statements for the same month.
```

A run reads about 19,000 balances, which looks like bulk extraction to the KMS token bucket. The Volume table gives this run alone an allowance, and going over it still raises the alarm (PERMIT-082).

### 3.17 Existing records to a conversion process

Tidewater has 59,459 loans, and only 18,402 are active. It converts the active loans, the steward decides on the 41 with status `H`, and closed loans stay in the readable archive (MIG-043).

```markdown
# Convert Scheduled Payments
Code: CSP
Application: payments

## Stages
1. Receive the extract (automated)
2. Parse and validate (sub-process, in chunks)
3. Review exceptions (human, skill: data stewardship)
4. Commit (sub-process, in chunks)
5. Reconcile (automated)

## Who can start it
The Payments data steward, one bank at a time.

## Volume
| Items              | Expected per run | Decrypts per item | At once |
|--------------------|------------------|-------------------|---------|
| scheduled payments | 240,000          | 1                 | 50      |

## Rules
- CSP-001: A bank's load matches the extract by record count and by total amount.
- CSP-002: A row that fails validation is never dropped or repaired silently.
- CSP-003: A loaded record starts no follow-up, notice or other work.
- CSP-004: No scheduled payment is loaded twice.
```

Each bank's load runs in `shadow` first. A row with `99999999` as its due date goes to the steward rather than being guessed at.

**Data migration strategy (MIG-045):** the architects record whether each record type keeps its past.

| Record type | Future need | Decision | How it comes in |
|---|---|---|---|
| Scheduled Payment | Only what's still expected | Transferred record | Current state, with one `transferred` history entry |
| Loan | Current terms and status | Transferred record | Same; balance as last reported |
| Payment | Banks dispute the last 24 months | History kept | Loaded as Payment records, with `payment date` as an ordinary field |
| Balance as reported | Explains those payments | History kept, because Payment needs it | Loaded as records, for the same 24 months |

The balances are copies of what Anchor Core reported. The ledger stays authoritative (SCOPE-001).

**In-flight work (MIG-044):** a payment already nine days into a 15-day grace period needs a follow-up already waiting. At cutover, a small process starts Late Payment Follow-up for each `expected` payment, as of that moment. Loading the record alone would never start it.

## 4. Worked spec: Late Payment Follow-up

The Standard describes this process in one paragraph (Tidewater Payments, section 3.4). The method produces its rules. This excerpt adds to it and doesn't replace it.

- **LPF-001:** Follow-up starts only if the scheduled payment is still `expected` once the bank's grace days have passed.
- **LPF-002:** Follow-up acts only on a loan that is still active when the process wakes.
- **LPF-003:** A late fee is 5% of the amount due, with any fraction of a cent dropped, and is never less than $10.00.
- **LPF-004:** A bank with the fee waived parameter set is never charged a late fee.
- **LPF-005:** A government-guaranteed loan never enters late payment follow-up.
- **LPF-006:** A due date on a bank holiday in the bank's calendar moves to the bank's next business day.

Out of scope (MIG-039):

- Posting fees to the ledger. Anchor Core does this (SCOPE-001).
- Printing the past-due listing. `TWRPT44` went unread for 30 months.
- Payment plans for late borrowers. Banks arrange these by phone today; reopen if volume grows.

## 5. Parallel-run differences

| ID | Case | Old | New | Classification | Decider |
|---|---|---|---|---|---|
| DIFF-007 | Fee on $1,234.70 | $61.73 | $61.74 | **Kept quirk.** Banks were always billed this way; LPF-003 now says so | Payments owner |
| DIFF-009 | Due date `99999999` | No follow-up | Follow-up opened | **Regression.** The conversion treated it as a date; the build is fixed | Technical lead |
| DIFF-012 | Grace period across February | Starts 2 days early | Starts on time | **Fixed defect.** The old code counted 30-day months; 214 fees are reviewed | Payments owner |
| DIFF-015 | Due date on a bank holiday | No follow-up yet | Follow-up opened | **Discovered rule.** The old job read a bank calendar nobody remembered; LPF-006 added | Payments owner |

## 6. Cutover

| Slice | Order | Writer after cutover | Way back |
|---|---|---|---|
| Late follow-up, Harborside | 1 | Payments writes Scheduled Payment status | Re-enable `ACLP210` for that bank; reconcile fees sent since |
| Late follow-up, Pinecrest then Granite Valley | 2 | Same | Same |
| Funding notices | 3 | Payments writes Funding Notice | Re-point the queue; replay from the outbox |
| Remittance intake | 4 | Payments writes Payment; posts to Anchor Core | Connect:Direct route retained for one cycle |
| Payment correction | 5 | Payments; Anchor Core posts the result | `TWPY` stays read-only, then re-enabled if needed |
| Payoff quotes | 6 | Lending | BankLink route retained for one cycle |
| KeelTrack replacement | 7 | The product | Keel Street's IBM i kept read-only |
| Monthly statements | 8 | Payments writes statements; Anchor Core supplies balances | `TWSTMT` kept ready for one cycle |

Each slice is a Gravity Scheduling glide path, and each cutover date is a gravity point.

### Permits on the way there

| Rung | Payments holds | Data | Real effects |
|---|---|---|---|
| dev | Build permit | Constructed | None |
| test | Test permit | Constructed or irreversibly masked | None |
| shadow | Production permit naming only `shadow` (PERMIT-080) | Real, captured from Anchor Core | None: no route to Anchor Core, banks, or staff |
| production | New permit adding `production` (PERMIT-079) | Real | Real, starting with Harborside |

Payments is one application with six slices, so one permit covers them all. The first slice's parallel run earns the new permit. Each later slice links its own results before its feature flag is enabled (PERMIT-079).
