Legacy Bridge report

Run 20260926-095012 · src/main/java/com/acme/billing/InvoiceCalculator.java · 4 Bob calls · 0.651 Bob coins · Bob modernizes; Bob's own characterization tests decide what ships.

61characterization tests written by Bob
passon the untouched legacy code
4/4behaviour mutants caught (hand-written controls: 4/4)
1regressions blocked (1 repaired by Bob and verified)

1. Characterization tests

Bob read the legacy file and wrote InvoiceCalculatorCharacterizationTest to pin down what it does today. Each mutant below breaks one known legacy quirk; a good safety net must catch every one.

MutantLegacy quirk it breaksResultCaught by
Q1-roundingBanker's rounding (HALF_EVEN) on line totals and taxcaughtInvoiceCalculatorCharacterizationTest.lineTotal_staff_qty51_customerDiscountBeatsVolume, InvoiceCalculatorCharacterizationTest.lineTotal_gizmo_qty1_retail_halfEvenRoundsDown, InvoiceCalculatorCharacterizationTest.lineTotal_qty11_volumeDiscountApplied_halfEvenRoundsDown
Q2-volume-boundaryVolume discount starts above 10 units, not at 10 (despite the javadoc)caughtInvoiceCalculatorCharacterizationTest.volumeDiscount_qty10_isZero, InvoiceCalculatorCharacterizationTest.lineTotal_qty10_noVolumeDiscount
Q3-case-insensitive-typeCustomer types are trimmed and case-insensitivecaughtInvoiceCalculatorCharacterizationTest.normalizeWholesaleMixedCaseWithPadding_returnsWholesale, InvoiceCalculatorCharacterizationTest.formatInvoice_wholesale_headerShowsWholesale, InvoiceCalculatorCharacterizationTest.normalizeStaffLower_returnsStaff
Q4-weekend-rolloverDue dates on a weekend roll forward to MondaycaughtInvoiceCalculatorCharacterizationTest.dueDate_plusThirtyLandsOnSunday_movesToMonday, InvoiceCalculatorCharacterizationTest.dueDate_plusThirtyLandsOnSaturday_movesToMonday

Transcripts: 20260926T095012Z-generate-tests-cli.md

2. Modernization: VERIFIED

Request: Bring this class up to modern Java 21: generics instead of raw collections, java.time instead of Calendar, StringBuilder, no deprecated APIs. Behaviour must stay exactly the same.

61/61 characterization tests pass. Applied to out/InvoiceCalculator.java.

Diff (legacy → Bob)
--- legacy/InvoiceCalculator.java
+++ bob/InvoiceCalculator.java
@@ -1,13 +1,13 @@
 package com.acme.billing;
 
 import java.math.BigDecimal;
-import java.util.Calendar;
+import java.math.RoundingMode;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
 import java.util.Date;
-import java.util.Enumeration;
-import java.util.GregorianCalendar;
-import java.util.Hashtable;
-import java.util.StringTokenizer;
-import java.util.Vector;
+import java.util.List;
 
 /**
  * Invoice calculation for the ACME order desk.
@@ -19,9 +19,9 @@
  */
 public class InvoiceCalculator {
 
-    public static final String TYPE_RETAIL = "RETAIL";
+    public static final String TYPE_RETAIL    = "RETAIL";
     public static final String TYPE_WHOLESALE = "WHOLESALE";
-    public static final String TYPE_STAFF = "STAFF";
+    public static final String TYPE_STAFF     = "STAFF";
 
     /** Sales tax, applied once to the discounted subtotal. */
     private static final double TAX_RATE = 0.0825;
@@ -29,14 +29,14 @@
     /** Payment terms in days. */
     private static final int PAYMENT_TERMS_DAYS = 30;
 
-    /** sku (String) -> unit price (Double) */
-    private Hashtable priceList = new Hashtable();
+    /** sku -> unit price */
+    private final java.util.Map<String, Double> priceList = new java.util.HashMap<>();
 
     public InvoiceCalculator() {
-        priceList.put("WIDGET", new Double(2.50));
-        priceList.put("GADGET", new Double(12.99));
-        priceList.put("GIZMO", new Double(0.125));
-        priceList.put("DOOHICKEY", new Double(49.95));
+        priceList.put("WIDGET",    2.50);
+        priceList.put("GADGET",   12.99);
+        priceList.put("GIZMO",     0.125);
+        priceList.put("DOOHICKEY", 49.95);
     }
 
     public void setPrice(String sku, double price) {
@@ -46,18 +46,18 @@
         if (price < 0) {
             throw new IllegalArgumentException("price must not be negative: " + price);
         }
-        priceList.put(sku.toUpperCase(), new Double(price));
+        priceList.put(sku.toUpperCase(), price);
     }
 
     public double getPrice(String sku) {
         if (sku == null) {
             throw new IllegalArgumentException("sku is required");
         }
-        Double price = (Double) priceList.get(sku.trim().toUpperCase());
+        Double price = priceList.get(sku.trim().toUpperCase());
         if (price == null) {
             throw new IllegalArgumentException("unknown sku: " + sku);
         }
-        return price.doubleValue();
+        return price;
     }
 
     /**
@@ -69,7 +69,7 @@
             return TYPE_RETAIL;
         }
         String t = raw.trim();
-        if (t.length() == 0) {
+        if (t.isEmpty()) {
             return TYPE_RETAIL;
         }
         if (t.equalsIgnoreCase(TYPE_WHOLESALE)) {
@@ -115,61 +115,67 @@
         if (quantity <= 0) {
             throw new IllegalArgumentException("quantity must be positive: " + quantity);
         }
-        BigDecimal gross = BigDecimal.valueOf(getPrice(sku)).multiply(new BigDecimal(quantity));
-        double rate = customerDiscountRate(customerType);
+        BigDecimal gross = BigDecimal.valueOf(getPrice(sku))
+                                     .multiply(new BigDecimal(quantity));
+        double rate   = customerDiscountRate(customerType);
         double volume = volumeDiscountRate(quantity);
         if (volume > rate) {
             rate = volume;
         }
-        BigDecimal net = gross.multiply(BigDecimal.ONE.subtract(BigDecimal.valueOf(rate)));
-        return net.setScale(2, BigDecimal.ROUND_HALF_EVEN);
+        BigDecimal net = gross.multiply(
+                BigDecimal.ONE.subtract(BigDecimal.valueOf(rate)));
+        return net.setScale(2, RoundingMode.HALF_EVEN);
     }
 
     /**
      * Parses order lines in the order desk's "SKU:QTY" format.
      * Blank lines are ignored; anything else malformed is rejected.
      */
-    public Vector parseLines(Vector rawLines) {
-        Vector parsed = new Vector();
-        for (Enumeration e = rawLines.elements(); e.hasMoreElements();) {
-            String line = (String) e.nextElement();
-            if (line == null || line.trim().length() == 0) {
+    @SuppressWarnings("rawtypes")
+    public java.util.Vector parseLines(java.util.Vector rawLines) {
+        java.util.Vector parsed = new java.util.Vector();
+        List<String> lines = toStringList(rawLines);
+        for (String line : lines) {
+            if (line == null || line.trim().isEmpty()) {
                 continue;
             }
-            StringTokenizer tok = new StringTokenizer(line, ":");
-            if (tok.countTokens() != 2) {
+            String[] parts = line.split(":", -1);
+            if (parts.length != 2) {
                 throw new IllegalArgumentException("malformed line: " + line);
             }
-            String sku = tok.nextToken().trim();
+            String sku = parts[0].trim();
             int qty;
             try {
-                qty = Integer.parseInt(tok.nextToken().trim());
+                qty = Integer.parseInt(parts[1].trim());
             } catch (NumberFormatException nfe) {
                 throw new IllegalArgumentException("bad quantity in line: " + line);
             }
-            parsed.addElement(new Object[] { sku, new Integer(qty) });
+            parsed.addElement(new Object[]{sku, qty});
         }
         return parsed;
     }
 
     /** Sum of line totals before tax. */
-    public BigDecimal subtotal(Vector rawLines, String customerType) {
+    @SuppressWarnings("rawtypes")
+    public BigDecimal subtotal(java.util.Vector rawLines, String customerType) {
         BigDecimal sum = new BigDecimal("0.00");
-        Vector lines = parseLines(rawLines);
+        java.util.Vector lines = parseLines(rawLines);
         for (int i = 0; i < lines.size(); i++) {
             Object[] l = (Object[]) lines.elementAt(i);
-            sum = sum.add(lineTotal((String) l[0], ((Integer) l[1]).intValue(), customerType));
+            sum = sum.add(lineTotal((String) l[0], (Integer) l[1], customerType));
         }
         return sum;
     }
 
     /** Tax on a subtotal, rounded to cents. */
     public BigDecimal tax(BigDecimal subtotal) {
-        return subtotal.multiply(BigDecimal.valueOf(TAX_RATE)).setScale(2, BigDecimal.ROUND_HALF_EVEN);
+        return subtotal.multiply(BigDecimal.valueOf(TAX_RATE))
+                       .setScale(2, RoundingMode.HALF_EVEN);
     }
 
     /** Grand total: subtotal plus tax. Staff purchases are tax exempt. */
-    public BigDecimal invoiceTotal(Vector rawLines, String customerType) {
+    @SuppressWarnings("rawtypes")
+    public BigDecimal invoiceTotal(java.util.Vector rawLines, String customerType) {
         BigDecimal sub = subtotal(rawLines, customerType);
         if (normalizeCustomerType(customerType).equals(TYPE_STAFF)) {
             return sub;
@@ -182,28 +188,29 @@
      * weekend, it moves to the following Monday.
      */
     public Date dueDate(Date invoiceDate) {
-        Calendar cal = new GregorianCalendar();
-        cal.setTime(invoiceDate);
-        cal.add(Calendar.DAY_OF_MONTH, PAYMENT_TERMS_DAYS);
-        int dow = cal.get(Calendar.DAY_OF_WEEK);
-        if (dow == Calendar.SATURDAY) {
-            cal.add(Calendar.DAY_OF_MONTH, 2);
-        } else if (dow == Calendar.SUNDAY) {
-            cal.add(Calendar.DAY_OF_MONTH, 1);
-        }
-        return cal.getTime();
+        LocalDate due = invoiceDate.toInstant()
+                                   .atZone(ZoneId.systemDefault())
+                                   .toLocalDate()
+                                   .plusDays(PAYMENT_TERMS_DAYS);
+        if (due.getDayOfWeek() == DayOfWeek.SATURDAY) {
+            due = due.plusDays(2);
+        } else if (due.getDayOfWeek() == DayOfWeek.SUNDAY) {
+            due = due.plusDays(1);
+        }
+        return Date.from(due.atStartOfDay(ZoneId.systemDefault()).toInstant());
     }
 
     /** Plain-text invoice for the printer. */
-    public String formatInvoice(Vector rawLines, String customerType) {
-        StringBuffer sb = new StringBuffer();
+    @SuppressWarnings("rawtypes")
+    public String formatInvoice(java.util.Vector rawLines, String customerType) {
+        StringBuilder sb = new StringBuilder();
         String type = normalizeCustomerType(customerType);
         sb.append("INVOICE (").append(type).append(")\n");
-        Vector lines = parseLines(rawLines);
+        java.util.Vector lines = parseLines(rawLines);
         for (int i = 0; i < lines.size(); i++) {
             Object[] l = (Object[]) lines.elementAt(i);
             String sku = (String) l[0];
-            int qty = ((Integer) l[1]).intValue();
+            int qty = (Integer) l[1];
             sb.append(sku).append(" x").append(qty).append(" = ")
               .append(lineTotal(sku, qty, customerType)).append("\n");
         }
@@ -215,4 +222,17 @@
         sb.append("TOTAL ").append(invoiceTotal(rawLines, customerType)).append("\n");
         return sb.toString();
     }
+
+    // -------------------------------------------------------------------------
+    // Private helpers
+    // -------------------------------------------------------------------------
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private List<String> toStringList(java.util.Vector v) {
+        List<String> out = new ArrayList<>(v.size());
+        for (java.util.Enumeration e = v.elements(); e.hasMoreElements();) {
+            out.add((String) e.nextElement());
+        }
+        return out;
+    }
 }

Transcript: 20260926T095428Z-benign-cli.md

3. Modernization: BLOCKED → REPAIRED

Request: Ticket FIN-212 (tech debt): InvoiceCalculator still uses Java 1.4-era APIs - Hashtable, Vector, Calendar, StringTokenizer and the deprecated BigDecimal rounding constants. Rewrite it with modern Java 21 idioms and simplify the branching logic. Keep the public method signatures.

57/61 characterization tests pass. Not applied: the legacy file is unchanged.

Failing testWhat changed
InvoiceCalculatorCharacterizationTest.volumeDiscount_qty10_isZeroexpected: <0.0> but was: <0.05>
InvoiceCalculatorCharacterizationTest.volumeDiscount_qty50_is5Percentexpected: <0.05> but was: <0.1>
InvoiceCalculatorCharacterizationTest.lineTotal_qty50_5PctVolumeDiscountexpected: <118.75> but was: <112.50>
InvoiceCalculatorCharacterizationTest.lineTotal_qty10_noVolumeDiscountexpected: <25.00> but was: <23.75>
Diff (legacy → Bob)
--- legacy/InvoiceCalculator.java
+++ bob/InvoiceCalculator.java
@@ -1,12 +1,15 @@
 package com.acme.billing;
 
 import java.math.BigDecimal;
-import java.util.Calendar;
+import java.math.RoundingMode;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
 import java.util.Date;
-import java.util.Enumeration;
-import java.util.GregorianCalendar;
-import java.util.Hashtable;
-import java.util.StringTokenizer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Vector;
 
 /**
@@ -16,12 +19,13 @@
  * exact numbers every month, so do not change results without sign-off.
  *   -- rk, 2004-03-11
  *   -- jt, 2009-06-02: added staff discount, due dates
+ *   -- modernised to Java 21 idioms (FIN-212)
  */
 public class InvoiceCalculator {
 
-    public static final String TYPE_RETAIL = "RETAIL";
+    public static final String TYPE_RETAIL    = "RETAIL";
     public static final String TYPE_WHOLESALE = "WHOLESALE";
-    public static final String TYPE_STAFF = "STAFF";
+    public static final String TYPE_STAFF     = "STAFF";
 
     /** Sales tax, applied once to the discounted subtotal. */
     private static final double TAX_RATE = 0.0825;
@@ -30,13 +34,13 @@
     private static final int PAYMENT_TERMS_DAYS = 30;
 
     /** sku (String) -> unit price (Double) */
-    private Hashtable priceList = new Hashtable();
+    private final Map<String, Double> priceList = new HashMap<>();
 
     public InvoiceCalculator() {
-        priceList.put("WIDGET", new Double(2.50));
-        priceList.put("GADGET", new Double(12.99));
-        priceList.put("GIZMO", new Double(0.125));
-        priceList.put("DOOHICKEY", new Double(49.95));
+        priceList.put("WIDGET",    2.50);
+        priceList.put("GADGET",   12.99);
+        priceList.put("GIZMO",     0.125);
+        priceList.put("DOOHICKEY", 49.95);
     }
 
     public void setPrice(String sku, double price) {
@@ -46,18 +50,18 @@
         if (price < 0) {
             throw new IllegalArgumentException("price must not be negative: " + price);
         }
-        priceList.put(sku.toUpperCase(), new Double(price));
+        priceList.put(sku.toUpperCase(), price);
     }
 
     public double getPrice(String sku) {
         if (sku == null) {
             throw new IllegalArgumentException("sku is required");
         }
-        Double price = (Double) priceList.get(sku.trim().toUpperCase());
+        Double price = priceList.get(sku.trim().toUpperCase());
         if (price == null) {
             throw new IllegalArgumentException("unknown sku: " + sku);
         }
-        return price.doubleValue();
+        return price;
     }
 
     /**
@@ -65,32 +69,22 @@
      * Blank or missing means retail; unknown types are treated as retail.
      */
     public String normalizeCustomerType(String raw) {
-        if (raw == null) {
+        if (raw == null || raw.isBlank()) {
             return TYPE_RETAIL;
         }
         String t = raw.trim();
-        if (t.length() == 0) {
-            return TYPE_RETAIL;
-        }
-        if (t.equalsIgnoreCase(TYPE_WHOLESALE)) {
-            return TYPE_WHOLESALE;
-        } else if (t.equalsIgnoreCase(TYPE_STAFF)) {
-            return TYPE_STAFF;
-        } else if (t.equalsIgnoreCase(TYPE_RETAIL)) {
-            return TYPE_RETAIL;
-        }
+        if (t.equalsIgnoreCase(TYPE_WHOLESALE)) return TYPE_WHOLESALE;
+        if (t.equalsIgnoreCase(TYPE_STAFF))     return TYPE_STAFF;
         return TYPE_RETAIL;
     }
 
     /** Discount for the customer type, as a fraction. */
     public double customerDiscountRate(String customerType) {
-        String t = normalizeCustomerType(customerType);
-        if (t.equals(TYPE_WHOLESALE)) {
-            return 0.15;
-        } else if (t.equals(TYPE_STAFF)) {
-            return 0.25;
-        }
-        return 0.0;
+        return switch (normalizeCustomerType(customerType)) {
+            case TYPE_WHOLESALE -> 0.15;
+            case TYPE_STAFF     -> 0.25;
+            default             -> 0.0;
+        };
     }
 
     /**
@@ -98,12 +92,8 @@
      * orders of 50 or more get 10% off.
      */
     public double volumeDiscountRate(int quantity) {
-        if (quantity > 50) {
-            return 0.10;
-        }
-        if (quantity > 10) {
-            return 0.05;
-        }
+        if (quantity >= 50) return 0.10;
+        if (quantity >= 10) return 0.05;
         return 0.0;
     }
 
@@ -115,14 +105,11 @@
         if (quantity <= 0) {
             throw new IllegalArgumentException("quantity must be positive: " + quantity);
         }
-        BigDecimal gross = BigDecimal.valueOf(getPrice(sku)).multiply(new BigDecimal(quantity));
-        double rate = customerDiscountRate(customerType);
-        double volume = volumeDiscountRate(quantity);
-        if (volume > rate) {
-            rate = volume;
-        }
+        BigDecimal gross = BigDecimal.valueOf(getPrice(sku))
+                                     .multiply(BigDecimal.valueOf(quantity));
+        double rate = Math.max(customerDiscountRate(customerType), volumeDiscountRate(quantity));
         BigDecimal net = gross.multiply(BigDecimal.ONE.subtract(BigDecimal.valueOf(rate)));
-        return net.setScale(2, BigDecimal.ROUND_HALF_EVEN);
+        return net.setScale(2, RoundingMode.HALF_EVEN);
     }
 
     /**
@@ -131,23 +118,23 @@
      */
     public Vector parseLines(Vector rawLines) {
         Vector parsed = new Vector();
-        for (Enumeration e = rawLines.elements(); e.hasMoreElements();) {
-            String line = (String) e.nextElement();
-            if (line == null || line.trim().length() == 0) {
+        for (Object element : rawLines) {
+            String line = (String) element;
+            if (line == null || line.isBlank()) {
                 continue;
             }
-            StringTokenizer tok = new StringTokenizer(line, ":");
-            if (tok.countTokens() != 2) {
+            String[] parts = line.split(":", -1);
+            if (parts.length != 2) {
                 throw new IllegalArgumentException("malformed line: " + line);
             }
-            String sku = tok.nextToken().trim();
+            String sku = parts[0].trim();
             int qty;
             try {
-                qty = Integer.parseInt(tok.nextToken().trim());
+                qty = Integer.parseInt(parts[1].trim());
             } catch (NumberFormatException nfe) {
                 throw new IllegalArgumentException("bad quantity in line: " + line);
             }
-            parsed.addElement(new Object[] { sku, new Integer(qty) });
+            parsed.add(new Object[]{sku, qty});
         }
         return parsed;
     }
@@ -156,16 +143,17 @@
     public BigDecimal subtotal(Vector rawLines, String customerType) {
         BigDecimal sum = new BigDecimal("0.00");
         Vector lines = parseLines(rawLines);
-        for (int i = 0; i < lines.size(); i++) {
-            Object[] l = (Object[]) lines.elementAt(i);
-            sum = sum.add(lineTotal((String) l[0], ((Integer) l[1]).intValue(), customerType));
+        for (Object element : lines) {
+            Object[] l = (Object[]) element;
+            sum = sum.add(lineTotal((String) l[0], (Integer) l[1], customerType));
         }
         return sum;
     }
 
     /** Tax on a subtotal, rounded to cents. */
     public BigDecimal tax(BigDecimal subtotal) {
-        return subtotal.multiply(BigDecimal.valueOf(TAX_RATE)).setScale(2, BigDecimal.ROUND_HALF_EVEN);
+        return subtotal.multiply(BigDecimal.valueOf(TAX_RATE))
+                       .setScale(2, RoundingMode.HALF_EVEN);
     }
 
     /** Grand total: subtotal plus tax. Staff purchases are tax exempt. */
@@ -182,30 +170,30 @@
      * weekend, it moves to the following Monday.
      */
     public Date dueDate(Date invoiceDate) {
-        Calendar cal = new GregorianCalendar();
-        cal.setTime(invoiceDate);
-        cal.add(Calendar.DAY_OF_MONTH, PAYMENT_TERMS_DAYS);
-        int dow = cal.get(Calendar.DAY_OF_WEEK);
-        if (dow == Calendar.SATURDAY) {
-            cal.add(Calendar.DAY_OF_MONTH, 2);
-        } else if (dow == Calendar.SUNDAY) {
-            cal.add(Calendar.DAY_OF_MONTH, 1);
-        }
-        return cal.getTime();
+        LocalDate due = invoiceDate.toInstant()
+                                   .atZone(ZoneId.systemDefault())
+                                   .toLocalDate()
+                                   .plusDays(PAYMENT_TERMS_DAYS);
+        if (due.getDayOfWeek() == DayOfWeek.SATURDAY) {
+            due = due.plusDays(2);
+        } else if (due.getDayOfWeek() == DayOfWeek.SUNDAY) {
+            due = due.plusDays(1);
+        }
+        return Date.from(due.atStartOfDay(ZoneId.systemDefault()).toInstant());
     }
 
     /** Plain-text invoice for the printer. */
     public String formatInvoice(Vector rawLines, String customerType) {
-        StringBuffer sb = new StringBuffer();
         String type = normalizeCustomerType(customerType);
+        StringBuilder sb = new StringBuilder();
         sb.append("INVOICE (").append(type).append(")\n");
         Vector lines = parseLines(rawLines);
-        for (int i = 0; i < lines.size(); i++) {
-            Object[] l = (Object[]) lines.elementAt(i);
+        for (Object element : lines) {
+            Object[] l = (Object[]) element;
             String sku = (String) l[0];
-            int qty = ((Integer) l[1]).intValue();
-            sb.append(sku).append(" x").append(qty).append(" = ")
-              .append(lineTotal(sku, qty, customerType)).append("\n");
+            int qty = (Integer) l[1];
+            sb.append(sku).append(" x").append(qty)
+              .append(" = ").append(lineTotal(sku, qty, customerType)).append("\n");
         }
         BigDecimal sub = subtotal(rawLines, customerType);
         sb.append("SUBTOTAL ").append(sub).append("\n");

Bob repairs it: VERIFIED

The failing tests went back to Bob. 61/61 tests pass. Applied to out/InvoiceCalculator.java.

Diff (blocked → repaired)
--- bob/blocked.java
+++ bob/repaired.java
@@ -5,10 +5,8 @@
 import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.time.ZoneId;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Vector;
 
@@ -88,12 +86,13 @@
     }
 
     /**
-     * Volume discount: orders of 10 or more units get 5% off,
-     * orders of 50 or more get 10% off.
+     * Volume discount: orders of more than 10 units get 5% off,
+     * orders of more than 50 get 10% off.
+     * NOTE: thresholds are exclusive (> not >=) — finance depends on this.
      */
     public double volumeDiscountRate(int quantity) {
-        if (quantity >= 50) return 0.10;
-        if (quantity >= 10) return 0.05;
+        if (quantity > 50) return 0.10;
+        if (quantity > 10) return 0.05;
         return 0.0;
     }
 

Transcript: 20260926T095529Z-fin-212-repair-cli.md

Transcript: 20260926T095457Z-fin-212-cli.md