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 }