001package gudusoft.gsqlparser.demos.prettyscore; 002 003import gudusoft.gsqlparser.pp.score.PrettyScore; 004import gudusoft.gsqlparser.pp.score.json.JsonIn; 005 006import java.io.File; 007import java.io.IOException; 008import java.nio.charset.Charset; 009import java.nio.file.Files; 010import java.util.ArrayList; 011import java.util.Collections; 012import java.util.Comparator; 013import java.util.LinkedHashMap; 014import java.util.LinkedHashSet; 015import java.util.List; 016import java.util.Locale; 017import java.util.Map; 018import java.util.Random; 019import java.util.Set; 020import java.util.TreeMap; 021 022/** 023 * Operations over saved run records: finding-level diff between two runs, 024 * baseline regression check, corpus backlog (Markdown) and blind A/B 025 * acceptance samples. All keyed on {@code file|engine|source} and the 026 * position-independent finding fingerprint so a reformat does not break the 027 * alignment. 028 */ 029public final class RunTools { 030 031 private RunTools() { 032 } 033 034 @SuppressWarnings("unchecked") 035 public static Map<String, Object> loadRun(File f) throws IOException { 036 return JsonIn.parseObject(new String(Files.readAllBytes(f.toPath()), Charset.forName("UTF-8"))); 037 } 038 039 @SuppressWarnings("unchecked") 040 static List<Map<String, Object>> entries(Map<String, Object> run) { 041 List<Map<String, Object>> out = new ArrayList<Map<String, Object>>(); 042 for (Object o : JsonIn.arr(run, "entries")) out.add((Map<String, Object>) o); 043 return out; 044 } 045 046 static String key(Map<String, Object> e) { 047 return e.get("file") + "|" + e.get("engine") + "|" + e.get("source"); 048 } 049 050 /** Fixable, counted findings of an entry as {@code fingerprint -> finding}. */ 051 @SuppressWarnings("unchecked") 052 static Map<String, Map<String, Object>> fingerprints(Map<String, Object> e) { 053 Map<String, Map<String, Object>> out = new LinkedHashMap<String, Map<String, Object>>(); 054 for (Object o : JsonIn.arr(e, "findings")) { 055 Map<String, Object> f = (Map<String, Object>) o; 056 if (!Boolean.TRUE.equals(f.get("fixable"))) continue; 057 String fp = String.valueOf(f.get("fingerprint")); 058 // several findings can share a fingerprint (e.g. two overflow lines in one predicate); suffix them 059 String k = fp; 060 int n = 2; 061 while (out.containsKey(k)) k = fp + "#" + (n++); 062 out.put(k, f); 063 } 064 return out; 065 } 066 067 /** Result of {@link #diffRuns}. */ 068 public static final class Diff { 069 public final List<String> fixed = new ArrayList<String>(); 070 public final List<String> added = new ArrayList<String>(); 071 public final List<String> persisting = new ArrayList<String>(); 072 /** Entries present in A but errored or absent in B: their findings are neither fixed nor persisting. */ 073 public final List<String> unavailable = new ArrayList<String>(); 074 public int newMajor; 075 public final StringBuilder text = new StringBuilder(); 076 } 077 078 public static Diff diffRuns(Map<String, Object> a, Map<String, Object> b) { 079 Diff d = new Diff(); 080 Map<String, Map<String, Object>> ea = new LinkedHashMap<String, Map<String, Object>>(); 081 for (Map<String, Object> e : entries(a)) ea.put(key(e), e); 082 Map<String, Map<String, Object>> eb = new LinkedHashMap<String, Map<String, Object>>(); 083 for (Map<String, Object> e : entries(b)) eb.put(key(e), e); 084 Set<String> keys = new LinkedHashSet<String>(ea.keySet()); 085 keys.addAll(eb.keySet()); 086 for (String k : keys) { 087 Map<String, Object> ebk = eb.get(k); 088 if (ebk == null || ebk.containsKey("error")) { 089 d.unavailable.add(k + (ebk == null ? " (missing in B)" : " (B error: " + ebk.get("error") + ")")); 090 continue; 091 } 092 Map<String, Map<String, Object>> fa = ea.containsKey(k) ? fingerprints(ea.get(k)) : new LinkedHashMap<String, Map<String, Object>>(); 093 Map<String, Map<String, Object>> fb = fingerprints(ebk); 094 for (String fp : fa.keySet()) { 095 if (fb.containsKey(fp)) d.persisting.add(k + " " + fp); 096 else d.fixed.add(k + " " + fp); 097 } 098 for (Map.Entry<String, Map<String, Object>> e : fb.entrySet()) { 099 if (fa.containsKey(e.getKey())) continue; 100 String sev = String.valueOf(e.getValue().get("severity")); 101 d.added.add(k + " " + e.getKey() + " [" + sev + "] " + e.getValue().get("evidence")); 102 if ("major".equals(sev) || "bug".equals(sev)) d.newMajor++; 103 } 104 } 105 d.text.append(String.format(Locale.ROOT, "fixed %d, new %d (%d major/bug), persisting %d, unavailable %d%n", 106 d.fixed.size(), d.added.size(), d.newMajor, d.persisting.size(), d.unavailable.size())); 107 summaryLine(d.text, "A", a); 108 summaryLine(d.text, "B", b); 109 if (!d.fixed.isEmpty()) { d.text.append("FIXED:\n"); for (String s : d.fixed) d.text.append(" ").append(s).append('\n'); } 110 if (!d.added.isEmpty()) { d.text.append("NEW:\n"); for (String s : d.added) d.text.append(" ").append(s).append('\n'); } 111 if (!d.unavailable.isEmpty()) { d.text.append("UNAVAILABLE:\n"); for (String s : d.unavailable) d.text.append(" ").append(s).append('\n'); } 112 if (!d.persisting.isEmpty()) { d.text.append("PERSISTING:\n"); for (String s : d.persisting) d.text.append(" ").append(s).append('\n'); } 113 return d; 114 } 115 116 @SuppressWarnings("unchecked") 117 static void summaryLine(StringBuilder sb, String label, Map<String, Object> run) { 118 Map<String, Object> summary = JsonIn.obj(run, "summary"); 119 sb.append(label).append(": "); 120 for (Map.Entry<String, Object> e : summary.entrySet()) { 121 Map<String, Object> s = (Map<String, Object>) e.getValue(); 122 if (s.containsKey("mean_ratio")) { 123 sb.append(String.format(Locale.ROOT, "%s mean_ratio=%.3f min=%.3f gate_fail=%s; ", e.getKey(), 124 JsonIn.dbl(s, "mean_ratio", 0), JsonIn.dbl(s, "min_ratio", 0), s.get("gate_failures"))); 125 } else if (s.containsKey("pp2_recovery_ratio")) { 126 sb.append(String.format(Locale.ROOT, "degraded pp2_recovery_ratio=%.3f; ", JsonIn.dbl(s, "pp2_recovery_ratio", 0))); 127 } 128 } 129 sb.append('\n'); 130 } 131 132 /** Baseline check: mean ratio must not drop by more than {@code maxDrop}; no new gate failures. Returns problems (empty = ok). */ 133 @SuppressWarnings("unchecked") 134 public static List<String> checkBaseline(Map<String, Object> baseline, Map<String, Object> current, double maxDrop) { 135 List<String> problems = new ArrayList<String>(); 136 for (String k : new String[]{"version", "vendor", "profile", "calibration", "seed", "degraded_samples", "engines", "ast"}) { 137 Object b = baseline.get(k), c = current.get(k); 138 if (b == null || c == null || !String.valueOf(b).equals(String.valueOf(c))) { 139 problems.add("run identity differs on " + k + ": baseline " + b + ", current " + c); 140 } 141 } 142 if (!problems.isEmpty()) return problems; // incomparable runs: do not read scores into it 143 Map<String, Object> sb = JsonIn.obj(baseline, "summary"); 144 Map<String, Object> sc = JsonIn.obj(current, "summary"); 145 for (Map.Entry<String, Object> e : sb.entrySet()) { 146 Map<String, Object> b = (Map<String, Object>) e.getValue(); 147 Map<String, Object> c = (Map<String, Object>) sc.get(e.getKey()); 148 if (c == null) { problems.add("engine " + e.getKey() + " missing from current run"); continue; } 149 String ratioKey = b.containsKey("mean_ratio") ? "mean_ratio" : "pp2_recovery_ratio"; 150 double rb = JsonIn.dbl(b, ratioKey, 0), rc = JsonIn.dbl(c, ratioKey, 0); 151 if (rb - rc > maxDrop) { 152 problems.add(String.format(Locale.ROOT, "%s %s dropped %.3f -> %.3f (max drop %.3f)", e.getKey(), ratioKey, rb, rc, maxDrop)); 153 } 154 } 155 Map<String, Map<String, Object>> eb = new LinkedHashMap<String, Map<String, Object>>(); 156 for (Map<String, Object> e : entries(baseline)) eb.put(key(e), e); 157 Set<String> currentKeys = new LinkedHashSet<String>(); 158 for (Map<String, Object> e : entries(current)) currentKeys.add(key(e)); 159 for (String k : eb.keySet()) { 160 if (!currentKeys.contains(k)) problems.add("entry missing from current run: " + k); 161 } 162 for (Map<String, Object> e : entries(current)) { 163 Map<String, Object> b = eb.get(key(e)); 164 if (b == null) { 165 if (e.containsKey("error")) problems.add("new error: " + key(e) + " " + e.get("error")); 166 else problems.add("entry not in baseline: " + key(e)); 167 continue; 168 } 169 if (Boolean.TRUE.equals(b.get("gate_passed")) && e.containsKey("gate_passed") && !Boolean.TRUE.equals(e.get("gate_passed"))) { 170 problems.add("new gate failure: " + key(e)); 171 } 172 if (!b.containsKey("error") && e.containsKey("error")) { 173 problems.add("new error: " + key(e) + " " + e.get("error")); 174 } 175 } 176 return problems; 177 } 178 179 /** Corpus-level improvement backlog as Markdown, aggregated by rule over fixable findings. */ 180 @SuppressWarnings("unchecked") 181 public static String backlog(Map<String, Object> run, String engineFilter) { 182 Map<String, List<Object[]>> byRule = new TreeMap<String, List<Object[]>>(); // rule -> [{gain, file, line, evidence}] 183 Map<String, Set<String>> filesHit = new TreeMap<String, Set<String>>(); 184 int files = 0; 185 for (Map<String, Object> e : entries(run)) { 186 if (!"clean".equals(e.get("source")) || e.containsKey("error")) continue; 187 if (engineFilter != null && !engineFilter.equals(e.get("engine"))) continue; 188 files++; 189 double attainable = JsonIn.dbl(e, "attainable", 100.0); 190 if (attainable <= 0) attainable = 100.0; 191 for (Object o : JsonIn.arr(e, "findings")) { 192 Map<String, Object> f = (Map<String, Object>) o; 193 if (!Boolean.TRUE.equals(f.get("fixable"))) continue; 194 String rule = String.valueOf(f.get("rule")); 195 List<Object[]> l = byRule.get(rule); 196 if (l == null) { l = new ArrayList<Object[]>(); byRule.put(rule, l); filesHit.put(rule, new LinkedHashSet<String>()); } 197 // gain in ratio points (score points / the entry's attainable), the quantity the loop tracks 198 l.add(new Object[]{JsonIn.dbl(f, "gain", 0) / attainable, e.get("file"), f.get("line"), f.get("evidence")}); 199 filesHit.get(rule).add(key(e)); 200 } 201 } 202 List<Map.Entry<String, List<Object[]>>> rows = new ArrayList<Map.Entry<String, List<Object[]>>>(byRule.entrySet()); 203 final Map<String, Double> totals = new LinkedHashMap<String, Double>(); 204 for (Map.Entry<String, List<Object[]>> r : rows) { 205 double t = 0; 206 for (Object[] x : r.getValue()) t += (Double) x[0]; 207 totals.put(r.getKey(), t); 208 } 209 Collections.sort(rows, new Comparator<Map.Entry<String, List<Object[]>>>() { 210 @Override 211 public int compare(Map.Entry<String, List<Object[]>> a, Map.Entry<String, List<Object[]>> b) { 212 return Double.compare(totals.get(b.getKey()), totals.get(a.getKey())); 213 } 214 }); 215 StringBuilder sb = new StringBuilder(); 216 sb.append("# PrettyScore backlog — ").append(run.get("vendor")).append(" / ").append(run.get("profile")) 217 .append(engineFilter == null ? "" : " / " + engineFilter).append(" (").append(run.get("calibration")).append(")\n\n"); 218 sb.append("Entries (file x engine): ").append(files).append(". Rows sorted by estimated corpus gain; unfixable findings excluded.\n\n"); 219 sb.append("| rank | rule | files_hit / total | total_ratio_gain | est_gain (mean ratio/entry) | target | examples |\n"); 220 sb.append("|---|---|---|---|---|---|---|\n"); 221 int rank = 1; 222 for (Map.Entry<String, List<Object[]>> r : rows) { 223 List<Object[]> l = r.getValue(); 224 Collections.sort(l, new Comparator<Object[]>() { 225 @Override public int compare(Object[] a, Object[] b) { return Double.compare((Double) b[0], (Double) a[0]); } 226 }); 227 double total = totals.get(r.getKey()); 228 String ex = example(l.get(0)) + "<br>" + example(l.get(l.size() / 2)) + "<br>" + example(l.get(l.size() - 1)); 229 sb.append(String.format(Locale.ROOT, "| %d | %s | %d / %d | %.3f | %.4f | %s | %s |%n", rank++, r.getKey(), 230 filesHit.get(r.getKey()).size(), files, total, files == 0 ? 0 : total / files, 231 PrettyScore.targetFor(r.getKey()).replace("|", "/"), ex.replace("|", "/"))); 232 } 233 return sb.toString(); 234 } 235 236 private static String example(Object[] x) { 237 String ev = String.valueOf(x[3]); 238 if (ev.length() > 80) ev = ev.substring(0, 77) + "..."; 239 return "`" + x[1] + ":" + x[2] + "` " + ev.replace("`", "'"); 240 } 241 242 /** 243 * Write {@code n} anonymised A/B pairs from two runs that kept their 244 * formatted text: {@code NN-left.sql} / {@code NN-right.sql} with random 245 * side assignment, plus {@code key.csv} (which side is run B) for the 246 * person tallying the votes. 247 */ 248 public static int acceptanceSample(Map<String, Object> a, Map<String, Object> b, int n, long seed, File outDir) throws IOException { 249 Map<String, Map<String, Object>> eb = new LinkedHashMap<String, Map<String, Object>>(); 250 for (Map<String, Object> e : entries(b)) eb.put(key(e), e); 251 List<String[]> candidates = new ArrayList<String[]>(); 252 for (Map<String, Object> e : entries(a)) { 253 Map<String, Object> other = eb.get(key(e)); 254 if (other == null) continue; 255 String ta = JsonIn.str(e, "formatted", null); 256 String tb = JsonIn.str(other, "formatted", null); 257 if (ta == null || tb == null || ta.equals(tb)) continue; 258 candidates.add(new String[]{key(e), ta, tb}); 259 } 260 if (candidates.isEmpty()) return 0; 261 Random rnd = new Random(seed); 262 Collections.shuffle(candidates, rnd); 263 if (!outDir.isDirectory() && !outDir.mkdirs()) throw new IOException("cannot create " + outDir); 264 StringBuilder keyCsv = new StringBuilder("pair,source,after_side\n"); 265 int written = 0; 266 for (int i = 0; i < candidates.size() && i < n; i++) { 267 String[] c = candidates.get(i); 268 boolean afterLeft = rnd.nextBoolean(); 269 String id = String.format(Locale.ROOT, "%02d", i + 1); 270 Files.write(new File(outDir, id + "-left.sql").toPath(), (afterLeft ? c[2] : c[1]).getBytes(Charset.forName("UTF-8"))); 271 Files.write(new File(outDir, id + "-right.sql").toPath(), (afterLeft ? c[1] : c[2]).getBytes(Charset.forName("UTF-8"))); 272 keyCsv.append(id).append(',').append(c[0].replace(',', ';')).append(',').append(afterLeft ? "left" : "right").append('\n'); 273 written++; 274 } 275 Files.write(new File(outDir, "key.csv").toPath(), keyCsv.toString().getBytes(Charset.forName("UTF-8"))); 276 return written; 277 } 278}