lib/closure/src/qualify/encode.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const json = @import("../receipt/root.zig").json;
  2 const model = @import("model.zig");
  3 const owner = @import("owner.zig");
  4 const pretty_json = @import("pretty").json;
  5 const schema = @import("../schema/root.zig");
  6 const std = @import("std");
  7 
  8 const JsonWriter = pretty_json.Writer;
  9 
 10 pub const Error = owner.Error || json.Error;
 11 
 12 pub fn freeze(
 13     plan: model.Plan,
 14     output: []u8,
 15 ) Error!model.Frozen {
 16     try owner.validatePlan(plan);
 17     const length = try json.measure(writePlan, .{plan});
 18     if (length > output.len or
 19         length > model.plan_bytes_max)
 20     {
 21         return error.ReceiptCapacityExceeded;
 22     }
 23     const encoded = try json.render(
 24         output,
 25         length,
 26         writePlan,
 27         .{plan},
 28     );
 29     var plan_digest = schema.Digest.zero();
 30     std.crypto.hash.sha2.Sha256.hash(
 31         encoded,
 32         &plan_digest.bytes,
 33         .{},
 34     );
 35     return .{
 36         .plan = plan,
 37         .sha256 = plan_digest,
 38         .bytes = encoded,
 39     };
 40 }
 41 
 42 pub fn receipt(
 43     frozen: model.Frozen,
 44     rows: []const model.Row,
 45     output: []u8,
 46 ) Error![]const u8 {
 47     const summary = try owner.evaluate(frozen, rows);
 48     const length = try json.measure(
 49         writeReceipt,
 50         .{ frozen, rows, summary },
 51     );
 52     if (length > output.len or
 53         length > model.receipt_bytes_max)
 54     {
 55         return error.ReceiptCapacityExceeded;
 56     }
 57     return try json.render(
 58         output,
 59         length,
 60         writeReceipt,
 61         .{ frozen, rows, summary },
 62     );
 63 }
 64 
 65 fn writePlan(writer: *std.Io.Writer, plan: model.Plan) std.Io.Writer.Error!void {
 66     {
 67         var stream = JsonWriter.init(writer, .minified);
 68         const sink = &stream;
 69         try sink.beginObject();
 70         try stringField(sink, "schema", "tiny.qualification.plan/v1");
 71         try stringField(sink, "claim", plan.claim.slice());
 72         try digestField(sink, "artifact_sha256", &plan.artifact_sha256);
 73         try digestField(
 74             sink,
 75             "execution_profile_sha256",
 76             &plan.execution_profile_sha256,
 77         );
 78         try unsignedField(sink, "cell_count", plan.cells.len);
 79         try unsignedField(sink, "bound_count", plan.bounds.len);
 80         try unsignedField(sink, "seed_count", plan.seeds.len);
 81         try sink.endObject();
 82         try sink.newline();
 83     }
 84     for (plan.cells) |*cell| {
 85         var stream = JsonWriter.init(writer, .minified);
 86         const sink = &stream;
 87         try sink.beginObject();
 88         try stringField(sink, "schema", "tiny.qualification.cell/v1");
 89         try stringField(sink, "claim", plan.claim.slice());
 90         try stringField(sink, "cell", cell.id.slice());
 91         try stringField(sink, "owner", cell.owner.slice());
 92         try enumField(sink, "requirement", cell.requirement);
 93         try enumField(sink, "kind", cell.kind);
 94         try enumField(sink, "evidence", cell.evidence);
 95         try stringField(sink, "workload", cell.workload.slice());
 96         try enumField(sink, "phase", cell.phase);
 97         try unsignedField(sink, "scale", cell.scale);
 98         try digestField(
 99             sink,
100             "artifact_sha256",
101             &plan.artifact_sha256,
102         );
103         try enumField(sink, "build_mode", cell.build_mode);
104         try digestField(
105             sink,
106             "execution_profile_sha256",
107             &plan.execution_profile_sha256,
108         );
109         try stringField(sink, "fault", cell.fault.slice());
110         try stringField(
111             sink,
112             "overload_counter",
113             cell.overload_counter.slice(),
114         );
115         try stringField(sink, "loss_counter", cell.loss_counter.slice());
116         try enumField(sink, "capacity_case", cell.capacity_case);
117         try unsignedField(sink, "capacity", cell.capacity);
118         try unsignedField(sink, "repetitions", cell.repetitions);
119         try unsignedField(sink, "work_max", cell.work_max);
120         try unsignedField(
121             sink,
122             "trace_steps_max",
123             cell.trace_steps_max,
124         );
125         try sink.objectField("threshold");
126         try sink.beginObject();
127         try enumField(sink, "kind", cell.threshold.kind);
128         try signedField(sink, "value", cell.threshold.value);
129         try unsignedField(
130             sink,
131             "confidence_ppm",
132             cell.threshold.confidence_ppm,
133         );
134         try sink.endObject();
135         try sink.objectField("limits");
136         try sink.beginArray();
137         const bound_start: usize = cell.bound_start;
138         const bound_end = bound_start + cell.bound_count;
139         for (plan.bounds[bound_start..bound_end]) |*bound| {
140             try sink.beginObject();
141             try stringField(sink, "name", bound.name.slice());
142             try unsignedField(sink, "value", bound.value);
143             try sink.endObject();
144         }
145         try sink.endArray();
146         try sink.objectField("seeds");
147         try sink.beginArray();
148         const seed_start: usize = cell.seed_start;
149         const seed_end = seed_start + cell.seed_count;
150         for (plan.seeds[seed_start..seed_end]) |seed| {
151             try sink.write(seed);
152         }
153         try sink.endArray();
154         try sink.endObject();
155         try sink.newline();
156     }
157 }
158 
159 fn writeReceipt(
160     writer: *std.Io.Writer,
161     frozen: model.Frozen,
162     rows: []const model.Row,
163     summary: model.Summary,
164 ) std.Io.Writer.Error!void {
165     for (frozen.plan.cells, rows) |*cell, *row| {
166         var stream = JsonWriter.init(writer, .minified);
167         const sink = &stream;
168         try sink.beginObject();
169         try stringField(sink, "schema", "tiny.qualification.result/v1");
170         try digestField(sink, "plan_sha256", &frozen.sha256);
171         try stringField(sink, "cell", row.cell.slice());
172         try enumField(sink, "requirement", cell.requirement);
173         try enumField(sink, "kind", cell.kind);
174         try enumField(sink, "evidence", row.evidence);
175         try enumField(sink, "verdict", row.verdict);
176         try enumField(sink, "capture", row.capture);
177         try booleanField(sink, "contract_pass", row.contract_pass);
178         try unsignedField(sink, "offered", row.outcomes.offered);
179         try unsignedField(sink, "accepted", row.outcomes.accepted);
180         try unsignedField(sink, "completed", row.outcomes.completed);
181         try unsignedField(sink, "rejected", row.outcomes.rejected);
182         try unsignedField(sink, "dropped", row.outcomes.dropped);
183         try unsignedField(
184             sink,
185             "outstanding",
186             row.outcomes.outstanding,
187         );
188         try digestField(sink, "before_digest", &row.before_digest);
189         try digestField(sink, "after_digest", &row.after_digest);
190         try digestField(
191             sink,
192             "effect_before_digest",
193             &row.effect_before_digest,
194         );
195         try digestField(
196             sink,
197             "effect_after_digest",
198             &row.effect_after_digest,
199         );
200         try unsignedField(sink, "high_water", row.high_water);
201         try unsignedField(sink, "work", row.work);
202         try unsignedField(
203             sink,
204             "allocation_count_after_seal",
205             row.allocation_count_after_seal,
206         );
207         try unsignedField(
208             sink,
209             "storage_address_before",
210             row.storage_address_before,
211         );
212         try unsignedField(
213             sink,
214             "storage_address_after",
215             row.storage_address_after,
216         );
217         try unsignedField(
218             sink,
219             "storage_capacity_before",
220             row.storage_capacity_before,
221         );
222         try unsignedField(
223             sink,
224             "storage_capacity_after",
225             row.storage_capacity_after,
226         );
227         try enumField(sink, "transition", row.transition);
228         try unsignedField(
229             sink,
230             "overload_count_before",
231             row.overload_count_before,
232         );
233         try unsignedField(
234             sink,
235             "overload_count_after",
236             row.overload_count_after,
237         );
238         try unsignedField(
239             sink,
240             "loss_count_before",
241             row.loss_count_before,
242         );
243         try unsignedField(
244             sink,
245             "loss_count_after",
246             row.loss_count_after,
247         );
248         try unsignedField(
249             sink,
250             "undeclared_effect_count",
251             row.undeclared_effect_count,
252         );
253         try unsignedField(
254             sink,
255             "undeclared_state_change_count",
256             row.undeclared_state_change_count,
257         );
258         try stringField(
259             sink,
260             "evidence_path",
261             row.evidence_path.slice(),
262         );
263         try stringField(sink, "trace_path", row.trace_path.slice());
264         try statisticsField(sink, row.statistics);
265         try sink.endObject();
266         try sink.newline();
267     }
268     {
269         var stream = JsonWriter.init(writer, .minified);
270         const sink = &stream;
271         try sink.beginObject();
272         try stringField(sink, "schema", "tiny.qualification.summary/v1");
273         try digestField(sink, "plan_sha256", &frozen.sha256);
274         try enumField(sink, "verdict", summary.verdict);
275         try unsignedField(sink, "passed", summary.passed);
276         try unsignedField(sink, "refuted", summary.refuted);
277         try unsignedField(sink, "inconclusive", summary.inconclusive);
278         try unsignedField(
279             sink,
280             "not_exercised",
281             summary.not_exercised,
282         );
283         try unsignedField(sink, "work", summary.work);
284         try unsignedField(sink, "high_water", summary.high_water);
285         try unsignedField(
286             sink,
287             "allocation_count_after_seal",
288             summary.allocation_count_after_seal,
289         );
290         try sink.endObject();
291         try sink.newline();
292     }
293 }
294 
295 fn statisticsField(
296     sink: *JsonWriter,
297     statistics: model.Statistics,
298 ) std.Io.Writer.Error!void {
299     try sink.objectField("statistics");
300     try sink.beginObject();
301     try booleanField(sink, "present", statistics.present);
302     try booleanField(
303         sink,
304         "interval_present",
305         statistics.interval_present,
306     );
307     try booleanField(sink, "interleaved", statistics.interleaved);
308     try digestField(
309         sink,
310         "baseline_profile_sha256",
311         &statistics.baseline_profile_sha256,
312     );
313     try digestField(
314         sink,
315         "candidate_profile_sha256",
316         &statistics.candidate_profile_sha256,
317     );
318     try unsignedField(sink, "sample_count", statistics.sample_count);
319     try unsignedField(sink, "baseline_count", statistics.baseline_count);
320     try unsignedField(sink, "candidate_count", statistics.candidate_count);
321     try signedField(sink, "min", statistics.minimum);
322     try signedField(sink, "p50", statistics.p50);
323     try signedField(sink, "p95", statistics.p95);
324     try signedField(sink, "p99", statistics.p99);
325     try signedField(sink, "max", statistics.maximum);
326     try signedField(sink, "interval_low", statistics.interval_low);
327     try signedField(sink, "interval_high", statistics.interval_high);
328     try unsignedField(
329         sink,
330         "baseline_high_water",
331         statistics.baseline_high_water,
332     );
333     try unsignedField(
334         sink,
335         "candidate_high_water",
336         statistics.candidate_high_water,
337     );
338     try sink.endObject();
339 }
340 
341 fn field(sink: *JsonWriter, name: []const u8) std.Io.Writer.Error!void {
342     try sink.objectField(name);
343 }
344 
345 fn stringField(
346     sink: *JsonWriter,
347     name: []const u8,
348     value: []const u8,
349 ) std.Io.Writer.Error!void {
350     try field(sink, name);
351     try sink.byteString(value);
352 }
353 
354 fn unsignedField(
355     sink: *JsonWriter,
356     name: []const u8,
357     value: anytype,
358 ) std.Io.Writer.Error!void {
359     try field(sink, name);
360     try sink.write(value);
361 }
362 
363 fn booleanField(
364     sink: *JsonWriter,
365     name: []const u8,
366     value: bool,
367 ) std.Io.Writer.Error!void {
368     try field(sink, name);
369     try sink.write(value);
370 }
371 
372 fn enumField(
373     sink: *JsonWriter,
374     name: []const u8,
375     value: anytype,
376 ) std.Io.Writer.Error!void {
377     try stringField(sink, name, @tagName(value));
378 }
379 
380 fn digestField(
381     sink: *JsonWriter,
382     name: []const u8,
383     value: *const schema.Digest,
384 ) std.Io.Writer.Error!void {
385     const hex = value.hex();
386     try stringField(sink, name, &hex);
387 }
388 
389 fn signedField(
390     sink: *JsonWriter,
391     name: []const u8,
392     value: i64,
393 ) std.Io.Writer.Error!void {
394     try field(sink, name);
395     try sink.write(value);
396 }
397 
398 fn makeName(value: []const u8) schema.Name {
399     return schema.Name.init(value) catch unreachable;
400 }
401 
402 fn descriptor(value: []const u8) schema.Descriptor {
403     return schema.Descriptor.init(value) catch unreachable;
404 }
405 
406 fn makeDigest(byte: u8) schema.Digest {
407     return .{ .bytes = @splat(byte) };
408 }
409 
410 fn testPlan() model.Plan {
411     const storage = struct {
412         const bounds = [_]model.Bound{.{
413             .name = makeName("entries"),
414             .value = 8,
415         }};
416         const seeds = [_]u64{11};
417         const cells = [_]model.Cell{.{
418             .id = makeName("ledger-proof"),
419             .owner = makeName("ledger"),
420             .requirement = .required,
421             .kind = .correctness,
422             .evidence = .proof,
423             .workload = descriptor("project one generation"),
424             .phase = .validation,
425             .scale = 1,
426             .build_mode = .release_safe,
427             .fault = descriptor("none"),
428             .overload_counter = makeName("ledger_overloads"),
429             .loss_counter = makeName("ledger_losses"),
430             .bound_start = 0,
431             .bound_count = 1,
432             .seed_start = 0,
433             .seed_count = 1,
434             .repetitions = 1,
435             .threshold = .{ .kind = .correctness },
436             .capacity_case = .none,
437             .capacity = 8,
438             .work_max = 100,
439             .trace_steps_max = 100,
440         }};
441     };
442     return .{
443         .claim = descriptor("ledger projection is exact"),
444         .artifact_sha256 = makeDigest(1),
445         .execution_profile_sha256 = makeDigest(2),
446         .cells = &storage.cells,
447         .bounds = &storage.bounds,
448         .seeds = &storage.seeds,
449     };
450 }
451 
452 fn testRow() model.Row {
453     return .{
454         .cell = makeName("ledger-proof"),
455         .evidence = .proof,
456         .verdict = .pass,
457         .capture = .complete,
458         .contract_pass = true,
459         .outcomes = .{
460             .offered = 1,
461             .accepted = 1,
462             .completed = 1,
463             .rejected = 0,
464             .dropped = 0,
465             .outstanding = 0,
466         },
467         .before_digest = makeDigest(3),
468         .after_digest = makeDigest(4),
469         .effect_before_digest = makeDigest(5),
470         .effect_after_digest = makeDigest(6),
471         .high_water = 1,
472         .work = 10,
473         .allocation_count_after_seal = 0,
474         .storage_address_before = 0x1000,
475         .storage_address_after = 0x1000,
476         .storage_capacity_before = 8,
477         .storage_capacity_after = 8,
478         .transition = .none,
479         .overload_count_before = 0,
480         .overload_count_after = 0,
481         .loss_count_before = 0,
482         .loss_count_after = 0,
483         .undeclared_effect_count = 0,
484         .undeclared_state_change_count = 0,
485         .statistics = .{},
486         .evidence_path = descriptor("proof.json"),
487         .trace_path = descriptor("proof.trace"),
488     };
489 }
490 
491 test "qualification plan and receipt encoding are canonical" {
492     var first_storage: [8 * 1_024]u8 = undefined;
493     var second_storage: [8 * 1_024]u8 = undefined;
494     const first = try freeze(testPlan(), &first_storage);
495     const second = try freeze(testPlan(), &second_storage);
496     try std.testing.expectEqualSlices(u8, first.bytes, second.bytes);
497     try std.testing.expect(first.sha256.eql(&second.sha256));
498     try std.testing.expect(
499         std.mem.indexOf(
500             u8,
501             first.bytes,
502             "\"schema\":\"tiny.qualification.cell/v1\"",
503         ) != null,
504     );
505     try std.testing.expect(
506         std.mem.indexOf(
507             u8,
508             first.bytes,
509             "\"limits\":[{\"name\":\"entries\",\"value\":8}]",
510         ) != null,
511     );
512     const rows = [_]model.Row{testRow()};
513     var receipt_storage: [16 * 1_024]u8 = undefined;
514     const encoded = try receipt(first, &rows, &receipt_storage);
515     try std.testing.expect(
516         std.mem.indexOf(
517             u8,
518             encoded,
519             "\"schema\":\"tiny.qualification.result/v1\"",
520         ) != null,
521     );
522     try std.testing.expect(
523         std.mem.indexOf(
524             u8,
525             encoded,
526             "\"verdict\":\"pass\"",
527         ) != null,
528     );
529     try std.testing.expect(
530         std.mem.indexOf(
531             u8,
532             encoded,
533             "\"allocation_count_after_seal\":0",
534         ) != null,
535     );
536     try std.testing.expect(
537         std.mem.indexOf(
538             u8,
539             encoded,
540             "\"overload_count_before\":0",
541         ) != null,
542     );
543 }
544 
545 test "qualification encoding preflights output without mutation" {
546     var output: [1]u8 = .{0xa5};
547     try std.testing.expectError(
548         error.ReceiptCapacityExceeded,
549         freeze(testPlan(), &output),
550     );
551     try std.testing.expectEqual(@as(u8, 0xa5), output[0]);
552 }