Gravity Migration

04 · Rules

Procedural code hides outcomes. Rules state them.

The Standard calls business rules the stable part. Years of code changes have blurred them into branches, flags and config. This chapter pulls them back out as plain sentences a business person can correct and a test can cite.

COBOL · at Tidewater

Nine lines, two rules and a secret.

This paragraph has calculated Tidewater's late fees since 1987. Two details decide real money: COMPUTE without ROUNDED drops the extra digits, and one bank is written straight into the code.

ACLP210 · 2200-ASSESS-FEEBefore
       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.
apps/payments/processes/late-payment-follow-up.mdAfter
## Parameters
| Parameter  | Type    | Meaning                         |
|------------|---------|---------------------------------|
| fee waived | boolean | The bank's loans have no late   |
|            |         | fee (Granite Valley: yes)       |

## Rules
- 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.

Worked case: $1,234.70 × 5% = $61.735, so the fee is $61.73. A rounding build charges $61.74.

The hard-coded bank

Bank 0417 is Granite Valley, waived by a 1998 agreement that nobody had filed anywhere else. Forking the process per bank is forbidden by the Standard, so a single IF becomes a tenant parameter, and the agreement finally has somewhere to be written down.

RPG on IBM i · at Tidewater

A system you're replacing still has rules.

KeelTrack came with the 2009 purchase of Keel Street Servicing, and a product will replace it. A vendor's feature list can't tell you whether Tidewater's real rule still holds, but a rule test can.

KTLAPSEBefore
chain (loanNo) COLLATPF;
if %found(COLLATPF) and CVGEXP < today - %days(30);
   LAPSEFLG = 'Y';
   update COLLATR;
endif;
migration/replace/insurance-tracking.mdAfter
- CIN-001: A loan's collateral counts as uninsured once
  its coverage has been expired for more than 30 days.

Becomes an acceptance test for the product. Older RPG hides the same logic in fixed-format indicators, so have someone who knows the codebase confirm it.

Java on the platform · at Tidewater

The rule was on the server, not in the repository.

Modern-looking code can still hide its rules. BankLink's payoff quote takes its validity period from a properties file that exists only on the WebSphere server. Its rounding, however, is explicit, so the two rules needed different evidence.

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

# on the server only
quote.valid.days=10
apps/lending/processes/payoff-quote.mdAfter
- 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.

These money rules differ: LPF-003 truncates and PQ-002 rounds. Each states its own (MIG-015).

Arithmetic

Money is where rewrites quietly go wrong.

BehaviorSourceWhat the rule must say
TruncationCOMPUTE without ROUNDED; RPG eval without (h)Drop or round, and to how many places
Field sizeA result silently losing high-order digitsThe largest valid value, and what happens above it
Intermediate precisionCompiler options, such as COBOL ARITHThe order of steps, and any rounding between them
Day countsHand-written 30/360 routinesThe convention, by name, with a worked case

What you leave behind · at Tidewater

A decision not to do something belongs in the spec too.

A migration is full of deliberate omissions. The evidence ledger is deleted with the source, so an omission recorded only there leaves nobody able to answer "why doesn't the new system do what the old one did?"

If you can test it, make it a rule, as with LPF-005. If you can't, because it's something not built, it goes in the spec's Out of scope section, where an AI assistant won't ask about it or add it.

apps/payments/processes/late-payment-follow-up.md
## Out of scope
- Posting fees to the ledger. Anchor Core does this
  (SCOPE-001).
- Printing the past-due listing. It went unread for
  30 months.
- Payment plans for late borrowers. Banks arrange these
  by phone today. Reopen if volume grows.

Rules

MIG-014 to MIG-017, MIG-039

  1. MIG-014

    A rule states the business outcome and never names the paragraph, step, or program it came from; that link belongs in the evidence ledger.

  2. MIG-015

    Every calculation rule states its precision, rounding, and order of operations exactly.

  3. MIG-016

    A hard-coded customer, date, or code value becomes a tenant parameter, a stated rule, or a retired behavior, by a recorded decision.

  4. MIG-017

    A capability marked replace still gets rules, because they become the acceptance test for the product.

  1. MIG-039

    A behavior deliberately not carried forward is recorded in the spec's Out of scope section, so the decision outlives the evidence ledger.

rule-card.md
# Rule card — <CODE>-nnn

- **Rule:** <one plain sentence stating an outcome; no program, step or paragraph names> (MIG-014)
- **Lives in:** <record type spec, if always true of the data; process spec, if true of one procedure>
- **Evidence:** EV-nnn, EV-nnn (MIG-005)

## If it calculates (MIG-015)
- **Precision:** <digits kept at each step>
- **Rounding:** truncate | half-up | half-even
- **Order of operations:** <what happens first>
- **Worked case:** <input> gives <output>

## Tests
- **Rule test:** Test<CODE>nnn — <what it asserts>
- **Captured cases:** <where they are, and how many> (MIG-029)

## Decision
- **Keep behavior as found?** yes | no → difference DIFF-nnn
- **Decided by:** <data steward or process owner> — YYYY-MM-DD