File size: 11,817 Bytes
617604d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
using System.Text.Json;
using CedarDotNet.Values;

namespace FindAJev.Bench;

/// <summary>The learning-loop checks: exhaustive properties of the admission and promotion policies, and the curation simulator.</summary>
public static class LearningChecks
{
    static readonly string[] AttackDomains = { "injection", "jailbreak", "harmful_request" };

    static Dictionary<string, Value> Ctx(string domain, string src, string consent, string lic, string pii, bool flagged, string cons, long conf, long margin) => new()
    {
        ["domain"] = domain, ["label_source"] = src, ["consent"] = consent, ["license_class"] = lic, ["pii_risk"] = pii,
        ["flagged"] = flagged, ["consensus"] = cons, ["confidence"] = conf, ["margin"] = margin,
    };

    /// <summary>Every combination of the admission/review attributes through Cedar versus a hand-written recomputation that shares no code with the policies.</summary>
    public static void AdmissionProperties(string root, CheckOutcome o)
    {
        var dir = Path.Combine(root, "policies");
        var findings = new List<string>(); long total = 0, bad = 0;
        var dom = new[] { "email_triage", "injection" };
        var src = new[] { "human", "outcome", "model" };
        var cons = new[] { "unanimous", "majority", "split" };
        foreach (var self in new long[] { 0, 1 })
        {
            var pe = new PolicyEngine(dir, new Dictionary<string, long> { ["admit_self_labels"] = self });
            long minConf = pe.Params["admit_min_conf"], reviewMargin = pe.Params["review_margin"];
            var confs = new long[] { minConf - 1, minConf }; var margins = new long[] { reviewMargin - 1, reviewMargin };
            var invariants = new Dictionary<string, int>();
            void Viol(string inv, string detail) { bad++; invariants[inv] = invariants.GetValueOrDefault(inv) + 1; if (findings.Count < 12) findings.Add($"[self={self}] {inv}: {detail}"); }
            foreach (var d in dom) foreach (var s in src) foreach (var consent in new[] { "yes", "no" }) foreach (var lic in new[] { "open", "research_only", "unverified" })
            foreach (var pii in new[] { "none", "low", "high" }) foreach (var fl in new[] { false, true }) foreach (var cs in cons) foreach (var cf in confs) foreach (var mg in margins)
            {
                var ctx = Ctx(d, s, consent, lic, pii, fl, cs, cf, mg);
                var tag = $"{d}/{s}/consent={consent}/{lic}/pii={pii}/flag={fl}/{cs}/conf={cf}/margin={mg}";
                var adm = pe.AuthorizeTrain("Admit", d, ctx); var rev = pe.AuthorizeTrain("Review", d, ctx);
                total += 2;
                if (adm.Error is not null || rev.Error is not null) { Viol("no-engine-error", tag + " " + adm.Error + rev.Error); continue; }
                var selfOk = s == "model" && self == 1 && cs == "unanimous" && cf >= minConf && mg >= reviewMargin && !fl && !AttackDomains.Contains(d);
                var wantAdmit = consent != "no" && lic == "open" && pii != "high" && (s is "human" or "outcome" || selfOk);
                if (adm.Allow != wantAdmit) Viol("admit-matches-recomputation", $"{tag}: cedar={adm.Allow} expected={wantAdmit}");
                if (adm.Allow && consent == "no") Viol("never-admit-without-consent", tag);
                if (adm.Allow && lic != "open") Viol("never-admit-non-open-source", tag);
                if (adm.Allow && pii == "high") Viol("never-admit-high-pii", tag);
                if (adm.Allow && s == "model" && fl) Viol("never-admit-flagged-self-label", tag);
                if (adm.Allow && s == "model" && AttackDomains.Contains(d)) Viol("never-self-label-attack-text", tag);
                if (adm.Allow && s == "model" && (cs != "unanimous")) Viol("self-label-needs-unanimity", tag);
                var wantReview = consent != "no" && pii != "high" && (fl || cs == "split" || mg < reviewMargin);
                if (rev.Allow != wantReview) Viol("review-matches-recomputation", $"{tag}: cedar={rev.Allow} expected={wantReview}");
                if (rev.Allow && (consent == "no" || pii == "high")) Viol("never-show-private-content-to-reviewers", tag);
                if (fl && consent != "no" && pii != "high" && !rev.Allow) Viol("flagged-always-reviewable", tag);
            }
        }
        o.Total = (int)total; o.Pass = (int)(total - Math.Min(total, bad)); o.Passed = bad == 0;
        o.Summary = $"{total} requests (2 settings of admit_self_labels x every combination), {bad} disagreement(s)";
        o.Findings = findings;
    }

    // ---------------------------------------------------------------------------------------------------- promotion gate
    static Dictionary<string, Value> Pctx(bool appr, bool comp, long core, long worst, long unsafeBp, long held, long err, long lat) => new()
    {
        ["human_approved"] = appr, ["comparable"] = comp, ["d_core_acc_bp"] = core, ["d_worst_suite_acc_bp"] = worst, ["d_unsafe_bp"] = unsafeBp,
        ["d_overblocked_bp"] = 0L, ["held_out_delta_bp"] = held, ["latency_ratio_pct"] = lat, ["errored"] = err, ["tests"] = 100L,
    };

    public static void PromotionGate(string root, CheckOutcome o)
    {
        var pe = new PolicyEngine(Path.Combine(root, "policies"));
        long maxUnsafe = pe.Params["promote_max_unsafe_rise_bp"], maxDrop = pe.Params["promote_max_suite_drop_bp"], maxLat = pe.Params["promote_max_latency_pct"];
        var findings = new List<string>(); int total = 0, bad = 0, allowed = 0;
        foreach (var pr in new[] { "Human", "Learner" }) foreach (var appr in new[] { true, false }) foreach (var comp in new[] { true, false })
        foreach (var core in new long[] { -1, 0, 50 }) foreach (var worst in new[] { -maxDrop - 1, -maxDrop, 0 }) foreach (var un in new[] { 0L, maxUnsafe, maxUnsafe + 1 })
        foreach (var held in new long[] { -1, 0 }) foreach (var err in new long[] { 0, 1 }) foreach (var lat in new[] { 100L, maxLat, maxLat + 1 })
        {
            var d = pe.AuthorizePromote(pr, "cand", Pctx(appr, comp, core, worst, un, held, err, lat)); total++;
            var want = pr == "Human" && appr && comp && core >= 0 && un <= maxUnsafe && worst >= -maxDrop && held >= 0 && err == 0 && lat <= maxLat;
            if (want) allowed++;
            if (d.Error is not null || d.Allow != want)
            { bad++; if (findings.Count < 12) findings.Add($"{pr} approved={appr} comparable={comp} core={core} worst={worst} unsafe={un} held={held} err={err} lat={lat}: cedar={d.Allow} expected={want} {d.Error}"); }
        }
        // demo on real results: each scored run against the most accurate other run of the default variant (informational)
        var demo = new List<object>();
        try
        {
            var runs = Promotion.LoadResults(root).Where(r => r.Variant == "" && r.Suites.Count > 0).ToList();
            var held = Data.Registry(root).Where(s => s.HeldOut).Select(s => s.Id).ToHashSet();
            foreach (var cand in runs) foreach (var champ in runs.Where(r => r.Id != cand.Id && r.Threads == cand.Threads).OrderByDescending(r => r.Accuracy).Take(1))
            {
                var ctx = Promotion.Context(cand, champ, true, held);
                var d = pe.AuthorizePromote("Human", cand.Id, ctx);
                demo.Add(new { candidate = cand.Id, champion = champ.Id, allow = d.Allow, by = d.Reasons, error = d.Error, comparable = ((BoolValue)ctx["comparable"]).Value });
            }
        }
        catch (Exception e) { findings.Add("real-result demo skipped: " + e.Message); }
        o.Total = total; o.Pass = total - bad; o.Passed = bad == 0;
        o.Summary = $"{total} boundary combinations ({allowed} expected to pass), {bad} disagreement(s); {demo.Count} real candidate/champion pair(s) evaluated";
        o.Findings = findings; o.Details = demo;
    }

    // ---------------------------------------------------------------------------------------------------- curation simulator
    /// <summary>Deterministic synthetic interactions, so the simulator's safety claims are tested even when no benchmark run has been recorded.</summary>
    public static List<SimExample> Synthetic(PolicyEngine pe, int n)
    {
        var rnd = new Random(20260929); var doms = pe.Packs.Keys.Concat(new[] { "pii", "retrieval" }).OrderBy(x => x, StringComparer.Ordinal).ToArray();
        var cons = new[] { "single", "unanimous", "unanimous", "majority", "split" };
        var res = new List<SimExample>();
        for (var i = 0; i < n; i++)
        {
            var wrong = rnd.NextDouble() < 0.25; var conf = wrong ? rnd.Next(20, 95) : rnd.Next(50, 100);
            res.Add(new SimExample($"syn-{i}", doms[rnd.Next(doms.Length)], "syn", new[] { wrong ? 1 : 0 }, new[] { 0 }, wrong, conf, rnd.Next(0, Math.Max(1, conf)),
                                   rnd.NextDouble() < (wrong ? 0.35 : 0.08), cons[rnd.Next(cons.Length)], 2, i % 3 == 0 ? "none" : "email"));
        }
        return res;
    }

    public static List<Curation.Scenario> Scenarios(double budgetPct, double annotatorAcc) => new()
    {
        new("naive self-training (admit everything)", new(), 0, 1, UseCedar: false),
        new("Cedar gate, humans review only", new(), budgetPct, annotatorAcc),
        new("Cedar gate + corroborated self-labels", new() { ["admit_self_labels"] = 1 }, budgetPct, annotatorAcc),
    };

    public static void CurationSim(string root, CheckOutcome o)
    {
        var findings = new List<string>(); var details = new List<object>(); int leaks = 0; int runs = 0;
        var pe = new PolicyEngine(Path.Combine(root, "policies"));
        var sets = new List<(string name, List<SimExample> ex)> { ("synthetic (1500 interactions)", Synthetic(pe, 1500)) };
        try
        {
            var preds = Curation.LoadPreds(root);
            if (preds.Count > 0)
            {
                var (ex, skipped) = Curation.BuildExamples(preds);
                sets.Add(($"recorded runs ({preds.Count} model(s): {string.Join(", ", preds.Keys)}; {skipped} test(s) skipped: options differ)", ex));
            }
            else findings.Add("no results/*.preds.jsonl yet: only the synthetic set was simulated (predictions are written by `run` from this version on)");
        }
        catch (Exception e) { findings.Add("recorded runs skipped: " + e.Message); }
        foreach (var (name, ex) in sets)
        {
            var rows = new List<object>();
            foreach (var sc in Scenarios(10, 0.98))
            {
                var r = Curation.Simulate(root, ex, sc, out _); runs++;
                var l = r.LeakNoConsent + r.LeakNotOpen + r.LeakPii + r.LeakAttackSelf;
                if (sc.UseCedar) leaks += l;
                if (sc.UseCedar && !sc.Overrides.ContainsKey("admit_self_labels") && r.WrongAdmittedSelf > 0) { leaks++; findings.Add($"{name} / {sc.Name}: {r.WrongAdmittedSelf} wrong model labels admitted although the default policy admits none"); }
                rows.Add(new { scenario = sc.Name, r.Examples, r.Reviewed, r.Admitted, r.AdmittedHuman, r.AdmittedSelf, r.WrongAdmitted, r.WrongAdmittedSelf,
                               labelNoisePct = r.Admitted == 0 ? 0 : Math.Round(100.0 * r.WrongAdmitted / r.Admitted, 2), yieldPct = r.Examples == 0 ? 0 : Math.Round(100.0 * r.Admitted / r.Examples, 1),
                               baseErrorPct = Math.Round(100 * r.BaseError, 2), errorsCaughtPct = Math.Round(100 * r.WrongErrorsCaught, 1), r.LeakNoConsent, r.LeakNotOpen, r.LeakPii, r.LeakAttackSelf });
            }
            details.Add(new { set = name, rows });
        }
        o.Total = runs; o.Pass = runs - (leaks > 0 ? 1 : 0); o.Passed = leaks == 0; o.Details = details; o.Findings = findings;
        o.Summary = $"{runs} scenario runs over {sets.Count} set(s); leaks through the Cedar gate: {leaks} (must be 0)";
    }
}