001package gudusoft.gsqlparser.demos.prettyscore;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.TGSqlParser;
005import gudusoft.gsqlparser.pp.para.GFmtOptFactory;
006import gudusoft.gsqlparser.pp.score.Dimension;
007import gudusoft.gsqlparser.pp.score.Finding;
008import gudusoft.gsqlparser.pp.score.PrettyScore;
009import gudusoft.gsqlparser.pp.score.ScoreProfile;
010import gudusoft.gsqlparser.pp.score.ScoreReport;
011import gudusoft.gsqlparser.pp.stmtformatter.FormatterFactory;
012
013import java.io.BufferedReader;
014import java.io.File;
015import java.io.FileInputStream;
016import java.io.IOException;
017import java.io.InputStreamReader;
018import java.nio.charset.Charset;
019import java.nio.file.Files;
020import java.util.ArrayList;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025
026/**
027 * Runs pp and/or pp2 over a benchmark manifest, scores every output with
028 * the fidelity gate on, optionally adds seeded syntax-degraded samples (pp2
029 * only), and produces the run record the CLI writes as JSON.
030 *
031 * <p>Run record layout (a published contract for {@code --diff-run} /
032 * {@code --diff-baseline} / {@code --backlog}):
033 * <pre>
034 * { "version": "prettyscore-run/0.1", "vendor", "profile", "engines": [...],
035 *   "entries": [ {file, engine, source: clean|degraded, degrade, parse_ok, gate_passed,
036 *                 score, attainable, ratio, dimensions: {id: score}, findings: [...], formatted} ],
037 *   "summary": { "<engine>": {files, scored, mean_score, mean_ratio, min_ratio, gate_failures, parse_failures},
038 *                "degraded": {samples, pp2_recovery_ratio, pp2_gate_failures} } }
039 * </pre>
040 */
041public final class CorpusRunner {
042
043    public static final String RUN_VERSION = "prettyscore-run/0.1";
044
045    private final EDbVendor vendor;
046    private final ScoreProfile profile;
047    private final PrettyScore scorer;
048    private final boolean keepText;
049
050    public CorpusRunner(EDbVendor vendor, ScoreProfile profile, boolean keepText) {
051        this(vendor, profile, keepText, true);
052    }
053
054    public CorpusRunner(EDbVendor vendor, ScoreProfile profile, boolean keepText, boolean astEnabled) {
055        this.vendor = vendor;
056        this.profile = profile;
057        this.keepText = keepText;
058        this.scorer = new PrettyScore().setAstEnabled(astEnabled);
059    }
060
061    /** Read a manifest: non-empty, non-comment lines are paths relative to {@code sqldir}. */
062    public static List<File> readManifest(File manifest, File sqldir) throws IOException {
063        List<File> out = new ArrayList<File>();
064        BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(manifest), Charset.forName("UTF-8")));
065        try {
066            String line;
067            while ((line = r.readLine()) != null) {
068                String t = line.trim();
069                if (t.isEmpty() || t.startsWith("#")) continue;
070                out.add(new File(sqldir, t));
071            }
072        } finally {
073            r.close();
074        }
075        return out;
076    }
077
078    public static String readText(File f) throws IOException {
079        return new String(Files.readAllBytes(f.toPath()), Charset.forName("UTF-8"));
080    }
081
082    /**
083     * @param files       the corpus files
084     * @param engines     "pp" and/or "pp2"
085     * @param degradeCount number of syntax-degraded samples to derive (0 = none); scored with pp2 only
086     * @param seed        degrader seed
087     */
088    public Map<String, Object> run(List<File> files, List<String> engines, int degradeCount, long seed, File relativeTo) {
089        Map<String, Object> run = new LinkedHashMap<String, Object>();
090        run.put("version", RUN_VERSION);
091        run.put("vendor", vendor.name());
092        run.put("profile", profile.getName());
093        run.put("calibration", profile.getCalibration());
094        run.put("engines", new ArrayList<String>(engines));
095        run.put("degraded_samples", degradeCount);
096        run.put("seed", seed);
097        run.put("ast", scorer.isAstEnabled());
098        List<Object> entries = new ArrayList<Object>();
099        List<String> texts = new ArrayList<String>();
100        List<String> names = new ArrayList<String>();
101        for (File f : files) {
102            String name = relativeTo == null ? f.getPath() : relativize(relativeTo, f);
103            String sql;
104            try {
105                sql = readText(f);
106            } catch (IOException e) {
107                entries.add(errorEntry(name, "-", "clean", null, "read failed: " + e.getMessage()));
108                continue;
109            }
110            texts.add(sql);
111            names.add(name);
112            for (String engine : engines) {
113                entries.add(scoreOne(name, engine, "clean", null, sql));
114            }
115        }
116        for (int i = 0; i < degradeCount && !texts.isEmpty(); i++) {
117            int k = i % texts.size();
118            SyntaxDegrader.Sample s = SyntaxDegrader.degrade(texts.get(k), vendor, seed, i);
119            if (s.stillParses) {
120                entries.add(errorEntry(names.get(k) + "#degraded" + i, "pp2", "degraded-skipped", s.kind.name().toLowerCase(Locale.ROOT) + ": " + s.description,
121                    "degradation still parses; excluded from the recovery corpus"));
122                continue;
123            }
124            entries.add(scoreOne(names.get(k) + "#degraded" + i, "pp2", "degraded", s.kind.name().toLowerCase(Locale.ROOT) + ": " + s.description, s.sql));
125        }
126        run.put("entries", entries);
127        run.put("summary", summarize(entries, engines));
128        return run;
129    }
130
131    static String relativize(File base, File f) {
132        String b = base.getAbsolutePath().replace('\\', '/');
133        String p = f.getAbsolutePath().replace('\\', '/');
134        if (p.startsWith(b)) {
135            p = p.substring(b.length());
136            if (p.startsWith("/")) p = p.substring(1);
137        }
138        return p;
139    }
140
141    /** Format {@code sql} with {@code engine} and score it against the original. */
142    public Map<String, Object> scoreOne(String name, String engine, String source, String degrade, String sql) {
143        String formatted;
144        boolean parseOk = true;
145        try {
146            TGSqlParser parser = new TGSqlParser(vendor);
147            parser.sqltext = sql;
148            if ("pp".equals(engine)) {
149                int rc = parser.parse();
150                if (rc != 0) {
151                    Map<String, Object> e = errorEntry(name, engine, source, degrade, "pp needs a clean parse: " + firstLine(parser.getErrormessage()));
152                    e.put("parse_ok", false);
153                    return e;
154                }
155                formatted = FormatterFactory.pp(parser, GFmtOptFactory.newInstance());
156            } else if ("pp2".equals(engine)) {
157                formatted = FormatterFactory.pp2(parser, GFmtOptFactory.newInstance());
158            } else if ("none".equals(engine)) {
159                formatted = sql;
160            } else {
161                return errorEntry(name, engine, source, degrade, "unknown engine " + engine);
162            }
163        } catch (Throwable t) {
164            return errorEntry(name, engine, source, degrade, "formatter threw " + t.getClass().getSimpleName() + ": " + firstLine(t.getMessage()));
165        }
166        try {
167            ScoreReport r = scorer.score(formatted, vendor, profile, sql, null);
168            return entry(name, engine, source, degrade, r, formatted);
169        } catch (Throwable t) {
170            return errorEntry(name, engine, source, degrade, "scorer threw " + t.getClass().getSimpleName() + ": " + firstLine(t.getMessage()));
171        }
172    }
173
174    private Map<String, Object> entry(String name, String engine, String source, String degrade, ScoreReport r, String formatted) {
175        Map<String, Object> e = new LinkedHashMap<String, Object>();
176        e.put("file", name);
177        e.put("engine", engine);
178        e.put("source", source);
179        e.put("degrade", degrade);
180        e.put("parse_evaluated", r.getParse().isEvaluated());
181        e.put("parse_ok", r.getParse().isEvaluated() && r.getParse().isOk());
182        e.put("gate_passed", r.isGatePassed());
183        e.put("score", r.getScore());
184        e.put("attainable", r.getAttainable());
185        e.put("ratio", r.getRatio());
186        Map<String, Object> dims = new LinkedHashMap<String, Object>();
187        Map<String, Double> weights = new LinkedHashMap<String, Double>();
188        for (Dimension d : r.getDimensions()) {
189            dims.put(d.getId(), d.getScore());
190            weights.put(d.getId(), d.getWeight());
191        }
192        e.put("dimensions", dims);
193        List<Object> fs = new ArrayList<Object>();
194        for (Finding f : r.getFindings()) {
195            if (f.isShadowed() || f.getImpact() <= 0) continue;
196            Map<String, Object> fm = new LinkedHashMap<String, Object>();
197            fm.put("rule", f.getRule());
198            fm.put("fingerprint", f.getFingerprint());
199            fm.put("line", f.getLine());
200            fm.put("col", f.getCol());
201            fm.put("severity", f.getSeverity().wireName());
202            fm.put("fixable", f.isFixable());
203            fm.put("impact", f.getImpact());
204            Double w = weights.get(f.getDimension());
205            fm.put("gain", f.getImpact() * (w == null ? 0 : w));
206            String ev = f.getEvidence();
207            fm.put("evidence", keepText || ev.length() <= 80 ? ev : ev.substring(0, 77) + "...");
208            fs.add(fm);
209        }
210        e.put("findings", fs);
211        if (keepText) {
212            // full runs keep the per-file metrics and the formatted text (needed by --acceptance-sample);
213            // baseline files (--no-text) stay small enough to live in git
214            e.put("metrics", r.getMetrics());
215            e.put("formatted", formatted);
216        }
217        return e;
218    }
219
220    private static Map<String, Object> errorEntry(String name, String engine, String source, String degrade, String error) {
221        Map<String, Object> e = new LinkedHashMap<String, Object>();
222        e.put("file", name);
223        e.put("engine", engine);
224        e.put("source", source);
225        e.put("degrade", degrade);
226        e.put("error", error);
227        return e;
228    }
229
230    private static String firstLine(String s) {
231        if (s == null) return "";
232        int i = s.indexOf('\n');
233        return (i < 0 ? s : s.substring(0, i)).trim();
234    }
235
236    @SuppressWarnings("unchecked")
237    public static Map<String, Object> summarize(List<Object> entries, List<String> engines) {
238        Map<String, Object> summary = new LinkedHashMap<String, Object>();
239        for (String engine : engines) {
240            int files = 0, scored = 0, gateFail = 0, parseFail = 0;
241            double sumScore = 0, sumRatio = 0, minRatio = 1.0;
242            for (Object o : entries) {
243                Map<String, Object> e = (Map<String, Object>) o;
244                if (!engine.equals(e.get("engine")) || !"clean".equals(e.get("source"))) continue;
245                files++;
246                if (e.containsKey("error")) { parseFail++; continue; }
247                scored++;
248                double score = ((Number) e.get("score")).doubleValue();
249                double ratio = ((Number) e.get("ratio")).doubleValue();
250                sumScore += score;
251                sumRatio += ratio;
252                if (ratio < minRatio) minRatio = ratio;
253                if (!Boolean.TRUE.equals(e.get("gate_passed"))) gateFail++;
254            }
255            Map<String, Object> s = new LinkedHashMap<String, Object>();
256            s.put("files", files);
257            s.put("scored", scored);
258            s.put("mean_score", scored == 0 ? 0 : sumScore / scored);
259            s.put("mean_ratio", scored == 0 ? 0 : sumRatio / scored);
260            s.put("min_ratio", scored == 0 ? 0 : minRatio);
261            s.put("gate_failures", gateFail);
262            s.put("parse_failures", parseFail);
263            summary.put(engine, s);
264        }
265        int samples = 0, scored = 0, gateFail = 0, errors = 0;
266        double sumRatio = 0;
267        for (Object o : entries) {
268            Map<String, Object> e = (Map<String, Object>) o;
269            if (!"degraded".equals(e.get("source"))) continue;
270            samples++;
271            if (e.containsKey("error")) { errors++; continue; }
272            scored++;
273            sumRatio += ((Number) e.get("ratio")).doubleValue();
274            if (!Boolean.TRUE.equals(e.get("gate_passed"))) gateFail++;
275        }
276        if (samples > 0) {
277            Map<String, Object> d = new LinkedHashMap<String, Object>();
278            d.put("samples", samples);
279            d.put("scored", scored);
280            d.put("errors", errors);
281            d.put("pp2_recovery_ratio", scored == 0 ? 0 : sumRatio / scored);
282            d.put("pp2_gate_failures", gateFail);
283            summary.put("degraded", d);
284        }
285        return summary;
286    }
287}