File size: 15,025 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using CedarDotNet.Values;

namespace FindAJev.Bench;

// =============================================================================================================================
// The learning loop, model-free parts: an audit ledger, the promotion gate's inputs, and a curation simulator that replays the
// predictions our runs already recorded through the Cedar admission policy. Nothing here trains a model.
// =============================================================================================================================

/// <summary>Append-only, hash-chained decision log (ledger/decisions.jsonl). Each line's `body` is hashed together with the previous hash.</summary>
public static class Ledger
{
    static string PathFor(string root) => Path.Combine(root, "ledger", "decisions.jsonl");
    static string Hash(string prev, string body) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(prev + "\n" + body))).ToLowerInvariant();

    public static void Append(string root, string kind, object payload)
    {
        var path = PathFor(root);
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        var prev = File.Exists(path) && File.ReadLines(path).LastOrDefault() is { Length: > 0 } last
            ? JsonDocument.Parse(last).RootElement.GetProperty("hash").GetString()! : new string('0', 64);
        var body = JsonSerializer.Serialize(new { ts = DateTime.UtcNow.ToString("o"), kind, payload });
        File.AppendAllText(path, JsonSerializer.Serialize(new { body, prev, hash = Hash(prev, body) }) + "\n");
    }

    /// <summary>Recompute the chain. Returns (ok, entries, first problem).</summary>
    public static (bool ok, int n, string? problem) Verify(string root)
    {
        var path = PathFor(root);
        if (!File.Exists(path)) return (true, 0, null);
        var prev = new string('0', 64); var n = 0;
        foreach (var line in File.ReadLines(path).Where(l => l.Length > 0))
        {
            var e = JsonDocument.Parse(line).RootElement;
            var body = e.GetProperty("body").GetString()!;
            if (e.GetProperty("prev").GetString() != prev) return (false, n, $"entry {n}: previous-hash link is broken");
            if (e.GetProperty("hash").GetString() != Hash(prev, body)) return (false, n, $"entry {n}: content does not match its hash");
            prev = e.GetProperty("hash").GetString()!; n++;
        }
        return (true, n, null);
    }
}

public static class Promotion
{
    /// <summary>Cedar context for "may this candidate replace the champion?", computed from two harness results. Deltas in basis points.</summary>
    public static Dictionary<string, Value> Context(RunResult cand, RunResult champ, bool humanApproved, IReadOnlySet<string> heldOut)
    {
        static double Rate(SuiteResult s, Func<SuiteResult, int> k) => s.Tests == 0 ? 0 : (double)k(s) / s.Tests;
        var shared = cand.Suites.Keys.Intersect(champ.Suites.Keys).ToList();
        long Bp(double d) => (long)Math.Round(d * 10000);
        var comparable = cand.Variant == champ.Variant && cand.Items == champ.Items && cand.SuitesRun.OrderBy(x => x).SequenceEqual(champ.SuitesRun.OrderBy(x => x))
                         && cand.PolicyParams.OrderBy(kv => kv.Key).SequenceEqual(champ.PolicyParams.OrderBy(kv => kv.Key));
        var suiteDeltas = shared.Select(k => Bp(cand.Suites[k].Accuracy - champ.Suites[k].Accuracy)).ToList();
        var heldDeltas = shared.Where(heldOut.Contains).Select(k => Bp(cand.Suites[k].Accuracy - champ.Suites[k].Accuracy)).ToList();
        return new Dictionary<string, Value>
        {
            ["human_approved"] = humanApproved,
            ["comparable"] = comparable,
            ["d_core_acc_bp"] = Bp(cand.Accuracy - champ.Accuracy),
            ["d_worst_suite_acc_bp"] = suiteDeltas.Count == 0 ? 0L : Math.Min(0, suiteDeltas.Min()),
            ["d_unsafe_bp"] = shared.Count == 0 ? 0L : shared.Max(k => Bp(Rate(cand.Suites[k], s => s.Unsafe) - Rate(champ.Suites[k], s => s.Unsafe))),
            ["d_overblocked_bp"] = shared.Count == 0 ? 0L : shared.Max(k => Bp(Rate(cand.Suites[k], s => s.Overblocked) - Rate(champ.Suites[k], s => s.Overblocked))),
            ["held_out_delta_bp"] = heldDeltas.Count == 0 ? 0L : heldDeltas.Min(),
            ["latency_ratio_pct"] = champ.P50Ms <= 0 ? 100L : (long)Math.Round(100 * cand.P50Ms / champ.P50Ms),
            ["errored"] = (long)cand.Suites.Values.Sum(s => s.Errored),
            ["tests"] = (long)cand.Suites.Values.Sum(s => s.Tests),
        };
    }

    public static IEnumerable<RunResult> LoadResults(string root) =>
        Directory.GetFiles(Path.Combine(root, "results"), "*.json").Select(f => JsonSerializer.Deserialize<RunResult>(File.ReadAllText(f), Json.Opts)!).Where(r => r.State == "Scored");
}

/// <summary>One logged interaction as the curation simulator sees it: what the models predicted, what an oracle flagged, and gold (evaluation only).</summary>
public sealed record SimExample(string Key, string Domain, string Suite, int[] Proposed, int[] Gold, bool ProposedWrong, int Confidence, int Margin,
                                bool Flagged, string Consensus, int Models, string ProposedLabel);

public static class Curation
{
    sealed record HeadRec(string t, int p, string? pl, int g, int c, int m, int n);
    sealed record PredRec(string k, string d, string suite, bool wrong, string[] flags, List<HeadRec> heads);
    static readonly JsonSerializerOptions Ci = new() { PropertyNameCaseInsensitive = true };

    /// <summary>Predictions written by benchmark runs (results/&lt;model&gt;.t&lt;threads&gt;.preds.jsonl, default variant only), keyed by model id.</summary>
    public static Dictionary<string, List<PredRecPublic>> LoadPreds(string root)
    {
        var res = new Dictionary<string, List<PredRecPublic>>();
        foreach (var f in Directory.GetFiles(Path.Combine(root, "results"), "*.preds.jsonl").OrderBy(x => x))
        {
            var m = Regex.Match(Path.GetFileName(f), @"^(.+)\.t(\d+)\.preds\.jsonl$");   // a hash suffix (variant runs) does not match: not comparable
            if (!m.Success || res.ContainsKey(m.Groups[1].Value)) continue;
            res[m.Groups[1].Value] = File.ReadLines(f).Where(l => l.Length > 0).Select(l => JsonSerializer.Deserialize<PredRecPublic>(l, Ci)!).ToList();
        }
        return res;
    }

    public sealed record HeadPublic(string t, int p, string? pl, int g, int c, int m, int n);
    public sealed record PredRecPublic(string k, string d, string suite, bool wrong, string[] flags, List<HeadPublic> heads);

    /// <summary>Join the models' predictions per test. The proposed label is the majority vote (ties: the more confident model); with one model it is that model's label.</summary>
    public static (List<SimExample> examples, int skipped) BuildExamples(Dictionary<string, List<PredRecPublic>> preds)
    {
        var byKey = new Dictionary<string, List<PredRecPublic>>();
        foreach (var recs in preds.Values) foreach (var r in recs) (byKey.TryGetValue(r.k, out var l) ? l : byKey[r.k] = new()).Add(r);
        var res = new List<SimExample>(); int skipped = 0;
        foreach (var (key, recs) in byKey)
        {
            var first = recs[0];
            // the models must have seen identical options in identical order, otherwise voting on indices is meaningless
            if (recs.Any(r => r.heads.Count != first.heads.Count || r.heads.Zip(first.heads).Any(z => z.First.g != z.Second.g || z.First.n != z.Second.n))) { skipped++; continue; }
            var proposed = new int[first.heads.Count]; var conf = new List<int>(); var marg = new List<int>(); var unanimousAll = true; var majorityAll = true;
            for (var h = 0; h < proposed.Length; h++)
            {
                var votes = recs.GroupBy(r => r.heads[h].p).Select(g => (label: g.Key, n: g.Count(), conf: g.Average(r => r.heads[h].c), marg: g.Average(r => r.heads[h].m)))
                                .OrderByDescending(v => v.n).ThenByDescending(v => v.conf).ToList();
                proposed[h] = votes[0].label; conf.Add((int)votes[0].conf); marg.Add((int)votes[0].marg);
                if (votes.Count > 1) unanimousAll = false;
                if (votes[0].n * 2 <= recs.Count) majorityAll = false;
            }
            var consensus = recs.Count == 1 ? "single" : unanimousAll ? "unanimous" : majorityAll ? "majority" : "split";
            var gold = first.heads.Select(x => x.g).ToArray();
            var pl = recs.SelectMany(r => r.heads.Where((x, i) => x.p == proposed[i] && x.pl is not null).Select(x => x.pl!)).FirstOrDefault() ?? "";
            res.Add(new SimExample(key, first.d, first.suite, proposed, gold, !proposed.SequenceEqual(gold), conf.Min(), marg.Min(),
                                   recs.Any(r => r.flags.Length > 0), consensus, recs.Count, pl));
        }
        return (res.OrderBy(e => e.Key, StringComparer.Ordinal).ToList(), skipped);
    }

    static int Stable(string s, int mod) => (int)(BitConverter.ToUInt32(SHA1.HashData(Encoding.UTF8.GetBytes(s)), 0) % (uint)mod);

    public sealed record Scenario(string Name, Dictionary<string, long> Overrides, double ReviewBudgetPct, double AnnotatorAccuracy, bool UseCedar = true);

    public sealed class ScenarioResult
    {
        public string Name { get; set; } = "";
        public int Examples { get; set; }
        public int Reviewed { get; set; }
        public int Admitted { get; set; }
        public int AdmittedHuman { get; set; }
        public int AdmittedSelf { get; set; }
        public int WrongAdmitted { get; set; }
        public int WrongAdmittedSelf { get; set; }
        public double BaseError { get; set; }             // wrong share among ALL proposed labels
        public double WrongErrorsCaught { get; set; }     // share of wrong proposals that a human saw
        public int LeakNoConsent { get; set; }            // independent audit of the gate: must be 0
        public int LeakNotOpen { get; set; }
        public int LeakPii { get; set; }
        public int LeakAttackSelf { get; set; }
    }

    /// <summary>Governance attributes of an example that do not come from the model (consent is simulated deterministically: 10% of interactions).</summary>
    public static (string consent, string license, string pii) Governance(string root, SimExample e)
    {
        var lic = "open";
        var gfile = Path.Combine(root, "policies", "governance.json");
        if (File.Exists(gfile) && System.Text.Json.Nodes.JsonNode.Parse(File.ReadAllText(gfile))!["domains"]![e.Domain]?["license_class"] is { } v) lic = v.GetValue<string>();
        var pii = e.Domain == "pii" ? (e.ProposedLabel.StartsWith("none") ? "low" : "high") : "low";
        return (Stable(e.Key, 10) == 0 ? "no" : "yes", lic, pii);
    }

    public static Dictionary<string, Value> TrainContext(string root, SimExample e, string labelSource)
    {
        var (consent, lic, pii) = Governance(root, e);
        return new Dictionary<string, Value>
        {
            ["domain"] = e.Domain, ["label_source"] = labelSource, ["consent"] = consent, ["license_class"] = lic, ["pii_risk"] = pii,
            ["flagged"] = e.Flagged, ["consensus"] = e.Consensus, ["confidence"] = (long)e.Confidence, ["margin"] = (long)e.Margin,
        };
    }

    public static ScenarioResult Simulate(string root, List<SimExample> ex, Scenario sc, out List<string> decisions)
    {
        decisions = new();
        var res = new ScenarioResult { Name = sc.Name, Examples = ex.Count, BaseError = ex.Count == 0 ? 0 : (double)ex.Count(e => e.ProposedWrong) / ex.Count };
        if (!sc.UseCedar)   // baseline: train on everything the model said, like a naive self-training loop
        {
            res.Admitted = res.AdmittedSelf = ex.Count; res.WrongAdmitted = res.WrongAdmittedSelf = ex.Count(e => e.ProposedWrong);
            foreach (var e in ex) { var (c, l, p) = Governance(root, e); res.LeakNoConsent += c == "no" ? 1 : 0; res.LeakNotOpen += l != "open" ? 1 : 0; res.LeakPii += p == "high" ? 1 : 0; }
            return res;
        }
        var pe = new PolicyEngine(Path.Combine(root, "policies"), sc.Overrides);
        // 1) which examples may a human look at, ranked by how suspicious they are (flag first, then the closest call)
        var candidates = new List<(SimExample e, PolicyDecision d)>();
        foreach (var e in ex)
        {
            var d = pe.AuthorizeTrain("Review", e.Domain, TrainContext(root, e, "model"));
            if (d.Error is not null) throw new InvalidOperationException(d.Error);
            // a human's time is only worth spending on examples that could be admitted once labelled (not no-consent, not a restricted source, not high PII)
            var admissible = pe.AuthorizeTrain("Admit", e.Domain, TrainContext(root, e, "human"));
            if (admissible.Error is not null) throw new InvalidOperationException(admissible.Error);
            if (d.Allow && admissible.Allow) candidates.Add((e, d));
        }
        var budget = (int)Math.Ceiling(ex.Count * sc.ReviewBudgetPct / 100.0);
        var reviewed = candidates.OrderByDescending(c => c.d.Reasons.Length).ThenBy(c => c.e.Margin).ThenBy(c => c.e.Key, StringComparer.Ordinal).Take(budget).Select(c => c.e.Key).ToHashSet();
        res.Reviewed = reviewed.Count;
        res.WrongErrorsCaught = ex.Count(e => e.ProposedWrong) == 0 ? 0 : (double)ex.Count(e => e.ProposedWrong && reviewed.Contains(e.Key)) / ex.Count(e => e.ProposedWrong);
        // 2) admission: reviewed examples now carry a human label (gold, except for annotator mistakes); the rest keep the model's label
        foreach (var e in ex)
        {
            var human = reviewed.Contains(e.Key);
            var wrong = human ? Stable("annotator" + e.Key, 1000) >= (int)(sc.AnnotatorAccuracy * 1000) : e.ProposedWrong;
            var d = pe.AuthorizeTrain("Admit", e.Domain, TrainContext(root, e, human ? "human" : "model"));
            if (d.Error is not null) throw new InvalidOperationException(d.Error);
            if (decisions.Count < 200) decisions.Add($"{e.Key}\t{(human ? "human" : "model")}\t{(d.Allow ? "admit" : "reject")}\t{string.Join(",", d.Reasons)}");
            if (!d.Allow) continue;
            res.Admitted++; if (human) res.AdmittedHuman++; else res.AdmittedSelf++;
            if (wrong) { res.WrongAdmitted++; if (!human) res.WrongAdmittedSelf++; }
            // independent audit of the gate (raw attributes, not Cedar): nothing may leak
            var (consent, lic, pii) = Governance(root, e);
            if (consent == "no") res.LeakNoConsent++;
            if (lic != "open") res.LeakNotOpen++;
            if (pii == "high") res.LeakPii++;
            if (!human && new[] { "injection", "jailbreak", "harmful_request" }.Contains(e.Domain)) res.LeakAttackSelf++;
        }
        return res;
    }
}