lib/choir/src/product/revision/record.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const simd = @import("simd");
  3 const binary = @import("../../serialization/binary/root.zig");
  4 
  5 const Bytes = simd.ScalableTag(u8);
  6 
  7 pub const schema_version: u32 = 2;
  8 
  9 /// Maximum number of nested values in a semantic projection.
 10 pub const maximum_projection_depth: u8 = 64;
 11 pub const ProjectionError = binary.WriteError || error{UnencodableProduct};
 12 pub const codec_limits = binary.Limits{
 13     .serialized_bytes = std.math.maxInt(u32),
 14     .string_bytes = std.math.maxInt(u32),
 15     .blob_bytes = std.math.maxInt(u32),
 16     .collection_entries = std.math.maxInt(u32),
 17     .total_entries = std.math.maxInt(u32),
 18 };
 19 pub const Writer = binary.Writer(codec_limits);
 20 pub const Reader = binary.Reader(codec_limits);
 21 
 22 pub const Version = struct {
 23     name: []const u8,
 24     version: u32,
 25 
 26     pub fn eql(self: Version, other: Version) bool {
 27         return self.version == other.version and std.mem.eql(u8, self.name, other.name);
 28     }
 29 };
 30 
 31 pub const Address = struct {
 32     producer: []const u8,
 33     source: []const u8,
 34     stage: []const u8,
 35     variant: []const u8,
 36 
 37     pub fn eql(self: Address, other: Address) bool {
 38         inline for (@typeInfo(Address).@"struct".field_names) |field| {
 39             if (!std.mem.eql(u8, @field(self, field), @field(other, field))) return false;
 40         }
 41         return true;
 42     }
 43 };
 44 
 45 /// A missing value records a negative observation, distinct from empty bytes.
 46 pub const Fact = struct {
 47     key: []const u8,
 48     value: ?[]const u8,
 49 
 50     pub fn eql(self: Fact, other: Fact) bool {
 51         if (!std.mem.eql(u8, self.key, other.key)) return false;
 52         if (self.value) |value| {
 53             return std.mem.eql(u8, value, other.value orelse return false);
 54         }
 55         return other.value == null;
 56     }
 57 };
 58 
 59 /// Dependency records contain exact semantic bytes, excluding publication events.
 60 pub const Dependency = struct {
 61     role: []const u8,
 62     exact: []const u8,
 63 };
 64 
 65 /// The compiler manifest is admission input, not encoded content. An owner
 66 /// interns it and decides identity through the interned bytes, so `encodeInputs`
 67 /// reads every other field and leaves this one to the store.
 68 pub const Inputs = struct {
 69     compiler_manifest: []const u8,
 70     versions: []const Version,
 71     pipeline: []const Version,
 72     options: []const u8,
 73     policy: []const u8,
 74     facts: []const Fact = &.{},
 75 };
 76 
 77 /// Decoded input fields. A record does not carry its compiler manifest, so this
 78 /// view cannot answer which compiler produced it; ask the owner that holds it.
 79 pub const InputView = struct {
 80     versions: Entries(Version),
 81     pipeline: Entries(Version),
 82     options: []const u8,
 83     policy: []const u8,
 84     facts: Entries(Fact),
 85     dependencies: Entries(Dependency),
 86     gate_manifest: []const u8,
 87 
 88     /// Checks declared gate identities only. Transported bytes do not establish
 89     /// that gates ran; callers obtain publication authority from a live Revision.
 90     pub fn requireGateDeclarations(self: InputView, required: []const Version) !void {
 91         for (required) |identity| {
 92             if (!try declaresGate(self.gate_manifest, identity)) return error.MissingGateEvidence;
 93         }
 94     }
 95 };
 96 
 97 fn declaresGate(bytes: []const u8, required: Version) !bool {
 98     var reader = try Reader.init(bytes);
 99     if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
100     _ = try reader.readString();
101     _ = try reader.readInt(u32);
102     var found = (try readGateIdentity(&reader)).eql(required);
103     const count = try reader.readCount();
104     for (0..count) |_| {
105         const identity = try readGateIdentity(&reader);
106         found = identity.eql(required) or found;
107     }
108     if (!reader.atEnd()) return error.InvalidRecord;
109     return found;
110 }
111 
112 fn readGateIdentity(reader: *Reader) !Version {
113     const identity = Version{
114         .name = try reader.readString(),
115         .version = try reader.readInt(u32),
116     };
117     _ = try reader.readBlob();
118     _ = try reader.readInt(u32);
119     return identity;
120 }
121 
122 pub const ClosureBounds = struct {
123     bytes: u32,
124     records: u32,
125     depth: u8,
126 };
127 
128 const ClosureFrame = struct {
129     dependencies: Entries(Dependency).Iterator,
130 };
131 
132 /// Embedded dependencies strictly decrease in byte extent, so exact cycles cannot
133 /// be represented. The iterative walk validates every closed record under bounds.
134 pub fn validateClosure(bytes: []const u8, bounds: ClosureBounds) !void {
135     if (bytes.len > bounds.bytes) return error.RecordLimit;
136     if (bounds.records == 0 or bounds.depth == 0) return error.RecordClosureLimit;
137     var stack: [255]ClosureFrame = undefined;
138     stack[0] = try closureFrame(bytes);
139     var depth: usize = 1;
140     var count: u32 = 1;
141     while (depth != 0) {
142         const dependency = try stack[depth - 1].dependencies.next() orelse {
143             depth -= 1;
144             continue;
145         };
146         if (count == bounds.records or depth == bounds.depth) return error.RecordClosureLimit;
147         stack[depth] = try closureFrame(dependency.exact);
148         depth += 1;
149         count += 1;
150     }
151 }
152 
153 fn closureFrame(bytes: []const u8) !ClosureFrame {
154     const exact = try decodeExact(bytes);
155     _ = try decodeAddress(exact.address);
156     const inputs = try decodeInputs(exact.inputs);
157     return .{ .dependencies = inputs.dependencies.iterator() };
158 }
159 
160 pub fn Entries(comptime T: type) type {
161     return struct {
162         bytes: []const u8,
163         count: u32,
164 
165         pub fn iterator(self: @This()) Iterator {
166             return .{
167                 .reader = Reader.init(self.bytes) catch unreachable,
168                 .remaining = self.count,
169             };
170         }
171 
172         pub const Iterator = struct {
173             reader: Reader,
174             remaining: u32,
175 
176             pub fn next(self: *Iterator) !?T {
177                 if (self.remaining == 0) return null;
178                 const result = try readEntry(T, &self.reader);
179                 self.remaining -= 1;
180                 return result;
181             }
182         };
183     };
184 }
185 
186 pub fn decodeAddress(bytes: []const u8) !Address {
187     var reader = try Reader.init(bytes);
188     if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
189     var result: Address = undefined;
190     inline for (@typeInfo(Address).@"struct".field_names, 1..) |field, tag| {
191         @field(result, field) = try readField(&reader, tag);
192     }
193     if (result.producer.len == 0 or result.stage.len == 0) return error.InvalidAddress;
194     if (!reader.atEnd()) return error.InvalidRecord;
195     return result;
196 }
197 
198 pub fn decodeInputs(bytes: []const u8) !InputView {
199     var reader = try Reader.init(bytes);
200     if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
201     const versions = try readEntries(Version, &reader, 2, "name");
202     const pipeline = try readEntries(Version, &reader, 3, null);
203     const options = try readField(&reader, 4);
204     const policy = try readField(&reader, 5);
205     const facts = try readEntries(Fact, &reader, 6, "key");
206     const dependencies = try readEntries(Dependency, &reader, 7, "role");
207     const gate_manifest = try readField(&reader, 8);
208     if (!reader.atEnd()) return error.InvalidRecord;
209     return .{
210         .versions = versions,
211         .pipeline = pipeline,
212         .options = options,
213         .policy = policy,
214         .facts = facts,
215         .dependencies = dependencies,
216         .gate_manifest = gate_manifest,
217     };
218 }
219 
220 fn readEntries(
221     comptime T: type,
222     reader: *Reader,
223     tag: u8,
224     comptime ordered: ?[]const u8,
225 ) !Entries(T) {
226     if (try reader.readInt(u8) != tag) return error.UnknownRequiredField;
227     const count = try reader.readCount();
228     const start = reader.offset;
229     var previous: ?[]const u8 = null;
230     for (0..count) |_| {
231         const item = try readEntry(T, reader);
232         if (ordered) |field| try orderedKey(&previous, @field(item, field));
233     }
234     return .{ .bytes = reader.bytes[start..reader.offset], .count = @intCast(count) };
235 }
236 
237 fn readEntry(comptime T: type, reader: *Reader) !T {
238     if (T == Version) {
239         const name = try reader.readString();
240         const version = try reader.readInt(u32);
241         if (version == 0 or name.len == 0) return error.InvalidVersion;
242         return .{ .name = name, .version = version };
243     } else if (T == Fact) {
244         return .{ .key = try reader.readString(), .value = try reader.readOptionalString() };
245     } else if (T == Dependency) {
246         const role = try reader.readString();
247         const exact = try reader.readBlob();
248         _ = try decodeExact(exact);
249         return .{ .role = role, .exact = exact };
250     } else @compileError("unknown canonical record collection");
251 }
252 
253 pub const Namespace = enum(u8) { root, operation, region, block, value, attribute };
254 pub const EntityCounts = [@typeInfo(Namespace).@"enum".field_names.len]u32;
255 
256 pub const Exact = struct {
257     address: []const u8,
258     inputs: []const u8,
259     image: []const u8,
260 
261     pub fn eql(self: Exact, other: Exact) bool {
262         return simd.equal(Bytes, self.address, other.address) and
263             simd.equal(Bytes, self.inputs, other.inputs) and
264             simd.equal(Bytes, self.image, other.image);
265     }
266 };
267 
268 const ValueTag = enum(u8) {
269     boolean = 1,
270     unsigned = 2,
271     signed = 3,
272     float_bits = 4,
273     enum_name = 5,
274     optional = 6,
275     sequence = 7,
276     structure = 8,
277     choice = 9,
278     bytes = 10,
279     unit = 11,
280 };
281 
282 /// Keep each owner's projection exhaustive when its option or plan type changes.
283 pub fn requireFields(comptime T: type, comptime names: []const []const u8) void {
284     const fields = @typeInfo(T).@"struct".field_names;
285     if (fields.len != names.len) {
286         @compileError("classify every field before publishing this record");
287     }
288     inline for (fields, names) |field, expected| {
289         if (!std.mem.eql(u8, field, expected)) {
290             @compileError("update the explicit semantic projection");
291         }
292     }
293 }
294 
295 /// Encode an owner's explicit semantic projection, never compiler pointers.
296 pub fn writeValue(writer: *Writer, value: anytype) ProjectionError!void {
297     try writeValueDepth(writer, value, 0);
298 }
299 
300 fn writeValueDepth(writer: *Writer, value: anytype, depth: u8) ProjectionError!void {
301     if (depth >= maximum_projection_depth) return error.UnencodableProduct;
302     const T = @TypeOf(value);
303     switch (@typeInfo(T)) {
304         .void => try valueTag(writer, .unit),
305         .bool => {
306             try valueTag(writer, .boolean);
307             try writer.writeBool(value);
308         },
309         .int => |info| {
310             if (info.bits > 64) @compileError("declare a bounded wide-integer encoding");
311             try valueTag(writer, if (info.signedness == .signed) .signed else .unsigned);
312             if (info.signedness == .signed) try writer.writeInt(i64, value) else {
313                 try writer.writeInt(u64, value);
314             }
315         },
316         .float => |info| {
317             try valueTag(writer, .float_bits);
318             try writer.writeInt(u16, info.bits);
319             const Bits = @Int(.unsigned, info.bits);
320             try writer.writeInt(Bits, @bitCast(value));
321         },
322         .@"enum" => {
323             try valueTag(writer, .enum_name);
324             try writer.writeString(@tagName(value));
325         },
326         .optional => {
327             try valueTag(writer, .optional);
328             try writer.writeBool(value != null);
329             if (value) |present| try writeValueDepth(writer, present, depth + 1);
330         },
331         .array, .pointer => try writeSequence(writer, value, depth),
332         .@"struct" => |info| {
333             try valueTag(writer, .structure);
334             try writer.writeCount(info.field_names.len);
335             inline for (info.field_names) |field| {
336                 try writer.writeString(field);
337                 try writeValueDepth(writer, @field(value, field), depth + 1);
338             }
339         },
340         .@"union" => {
341             try valueTag(writer, .choice);
342             try writer.writeString(@tagName(value));
343             switch (value) {
344                 inline else => |payload| try writeValueDepth(writer, payload, depth + 1),
345             }
346         },
347         else => @compileError("classify this semantic field before publication"),
348     }
349 }
350 
351 fn writeSequence(writer: *Writer, value: anytype, depth: u8) ProjectionError!void {
352     std.debug.assert(depth < maximum_projection_depth);
353     const info = @typeInfo(@TypeOf(value));
354     if (info == .pointer) {
355         if (info.pointer.size != .slice) {
356             @compileError("project references into exact revision ordinals");
357         }
358         if (info.pointer.child == u8) {
359             try valueTag(writer, .bytes);
360             try writer.writeBlob(value);
361             return;
362         }
363     }
364     try valueTag(writer, .sequence);
365     try writer.writeCount(value.len);
366     for (value) |item| try writeValueDepth(writer, item, depth + 1);
367 }
368 
369 fn valueTag(writer: *Writer, value: ValueTag) !void {
370     try writer.writeTag(value);
371 }
372 
373 pub fn encodeAddress(allocator: std.mem.Allocator, value: Address) ![]u8 {
374     if (value.producer.len == 0 or value.stage.len == 0) return error.InvalidAddress;
375     var writer = Writer.init(allocator);
376     defer writer.deinit();
377     var length: usize = 24;
378     inline for (@typeInfo(Address).@"struct".field_names) |field| {
379         length = try sizeAdd(length, @field(value, field).len);
380     }
381     try writer.bytes.ensureTotalCapacityPrecise(allocator, length);
382     try writer.writeInt(u32, schema_version);
383     inline for (@typeInfo(Address).@"struct".field_names, 1..) |field, tag| {
384         try writer.writeInt(u8, tag);
385         try writer.writeString(@field(value, field));
386     }
387     std.debug.assert(writer.bytes.items.len == length);
388     return writer.finish();
389 }
390 
391 pub fn encodeInputs(
392     allocator: std.mem.Allocator,
393     inputs: Inputs,
394     dependencies: []const Dependency,
395     gate_manifest: []const u8,
396 ) ![]u8 {
397     comptime std.debug.assert(@typeInfo(Inputs).@"struct".field_names.len == 6);
398     const versions = try sortedMap(Version, allocator, inputs.versions, "name");
399     defer allocator.free(versions);
400     const facts = try sortedMap(Fact, allocator, inputs.facts, "key");
401     defer allocator.free(facts);
402     const ordered_dependencies = try sortedMap(Dependency, allocator, dependencies, "role");
403     defer allocator.free(ordered_dependencies);
404     var writer = Writer.init(allocator);
405     defer writer.deinit();
406     const length = try inputsSize(inputs, dependencies, gate_manifest);
407     try writer.bytes.ensureTotalCapacityPrecise(allocator, length);
408     try writer.writeInt(u32, schema_version);
409     try writer.writeInt(u8, 2);
410     try writeVersions(&writer, versions, true);
411     try writer.writeInt(u8, 3);
412     try writeVersions(&writer, inputs.pipeline, false);
413     try fieldBlob(&writer, 4, inputs.options);
414     try fieldBlob(&writer, 5, inputs.policy);
415     try writer.writeInt(u8, 6);
416     try writeFacts(&writer, facts);
417     try writer.writeInt(u8, 7);
418     try writeDependencies(&writer, ordered_dependencies);
419     try fieldBlob(&writer, 8, gate_manifest);
420     std.debug.assert(writer.bytes.items.len == length);
421     return writer.finish();
422 }
423 
424 fn inputsSize(inputs: Inputs, dependencies: []const Dependency, gates: []const u8) !usize {
425     var size: usize = 39;
426     for ([_][]const u8{ inputs.options, inputs.policy, gates }) |bytes| {
427         size = try sizeAdd(size, bytes.len);
428     }
429     for ([_][]const Version{ inputs.versions, inputs.pipeline }) |versions| {
430         for (versions) |version| size = try sizeAdd(size, try sizeAdd(8, version.name.len));
431     }
432     for (inputs.facts) |fact| {
433         size = try sizeAdd(size, try sizeAdd(5, fact.key.len));
434         if (fact.value) |bytes| size = try sizeAdd(size, try sizeAdd(4, bytes.len));
435     }
436     for (dependencies) |dependency| {
437         size = try sizeAdd(size, try sizeAdd(8, dependency.role.len));
438         size = try sizeAdd(size, dependency.exact.len);
439     }
440     return size;
441 }
442 
443 fn sizeAdd(first: usize, second: usize) !usize {
444     const size = std.math.add(usize, first, second) catch return error.RecordOverflow;
445     if (size > codec_limits.serialized_bytes) return error.RecordLimit;
446     return size;
447 }
448 
449 fn sortedMap(
450     comptime T: type,
451     allocator: std.mem.Allocator,
452     source: []const T,
453     comptime key: []const u8,
454 ) ![]T {
455     const copy = try allocator.dupe(T, source);
456     errdefer allocator.free(copy);
457     std.mem.sort(T, copy, {}, struct {
458         fn less(_: void, first: T, second: T) bool {
459             return std.mem.lessThan(u8, @field(first, key), @field(second, key));
460         }
461     }.less);
462     var previous: ?[]const u8 = null;
463     for (copy) |item| try orderedKey(&previous, @field(item, key));
464     return copy;
465 }
466 
467 fn fieldBlob(writer: anytype, tag: u8, bytes: []const u8) !void {
468     try writer.writeInt(u8, tag);
469     try writer.writeBlob(bytes);
470 }
471 
472 fn writeVersions(writer: *Writer, versions: []const Version, sorted: bool) !void {
473     try writer.writeCount(versions.len);
474     var previous: ?[]const u8 = null;
475     for (versions) |version| {
476         if (version.version == 0 or version.name.len == 0) return error.InvalidVersion;
477         if (sorted) try orderedKey(&previous, version.name);
478         try writer.writeString(version.name);
479         try writer.writeInt(u32, version.version);
480     }
481 }
482 
483 fn writeFacts(writer: *Writer, facts: []const Fact) !void {
484     try writer.writeCount(facts.len);
485     var previous: ?[]const u8 = null;
486     for (facts) |fact| {
487         try orderedKey(&previous, fact.key);
488         try writer.writeString(fact.key);
489         try writer.writeOptionalString(fact.value);
490     }
491 }
492 
493 fn writeDependencies(writer: *Writer, dependencies: []const Dependency) !void {
494     try writer.writeCount(dependencies.len);
495     var previous: ?[]const u8 = null;
496     for (dependencies) |dependency| {
497         try orderedKey(&previous, dependency.role);
498         _ = try decodeExact(dependency.exact);
499         try writer.writeString(dependency.role);
500         try writer.writeBlob(dependency.exact);
501     }
502 }
503 
504 fn orderedKey(previous: *?[]const u8, key: []const u8) !void {
505     if (key.len == 0) return error.InvalidKey;
506     if (previous.*) |prior| {
507         if (!std.mem.lessThan(u8, prior, key)) return error.NoncanonicalOrder;
508     }
509     previous.* = key;
510 }
511 
512 pub fn encodeExact(allocator: std.mem.Allocator, exact: Exact) ![]u8 {
513     var writer = Writer.init(allocator);
514     defer writer.deinit();
515     try writeExact(&writer, exact);
516     return writer.finish();
517 }
518 
519 /// The caller retains ownership of the output storage. Preflight refuses a short
520 /// buffer before writing; the writer has no allocator capacity for growth.
521 pub fn encodeExactInto(output: []u8, exact: Exact) ![]u8 {
522     const length = try exactSize(exact.address.len, exact.inputs.len, exact.image.len);
523     if (length > output.len) return error.RecordLimit;
524     var writer = binary.FixedWriter(codec_limits).init(output);
525     defer writer.deinit();
526     try writeExact(&writer, exact);
527     std.debug.assert(writer.bytes.items.len == length);
528     std.debug.assert(writer.bytes.items.ptr == output.ptr);
529     return writer.finish();
530 }
531 
532 pub fn exactSize(address: usize, inputs: usize, image: usize) !usize {
533     const prefix = std.math.add(usize, address, inputs) catch return error.RecordOverflow;
534     const payload = std.math.add(usize, prefix, image) catch return error.RecordOverflow;
535     const length = std.math.add(usize, payload, 19) catch return error.RecordOverflow;
536     if (address > std.math.maxInt(u32) or inputs > std.math.maxInt(u32) or
537         image > std.math.maxInt(u32)) return error.RecordLimit;
538     return length;
539 }
540 
541 fn writeExact(writer: anytype, exact: Exact) !void {
542     comptime requireFields(Exact, &.{ "address", "inputs", "image" });
543     try writer.writeInt(u32, schema_version);
544     try fieldBlob(writer, 1, exact.address);
545     try fieldBlob(writer, 2, exact.inputs);
546     try fieldBlob(writer, 3, exact.image);
547 }
548 
549 pub fn decodeExact(bytes: []const u8) !Exact {
550     var reader = try Reader.init(bytes);
551     if (try reader.readInt(u32) != schema_version) return error.UnknownSchema;
552     const address = try readField(&reader, 1);
553     const inputs = try readField(&reader, 2);
554     const image = try readField(&reader, 3);
555     if (!reader.atEnd()) return error.InvalidRecord;
556     return .{ .address = address, .inputs = inputs, .image = image };
557 }
558 
559 fn readField(reader: *Reader, tag: u8) ![]const u8 {
560     if (try reader.readInt(u8) != tag) return error.UnknownRequiredField;
561     return reader.readBlob();
562 }
563 
564 pub fn validateObservations(declared: []const Fact, observed: []const Fact) !void {
565     if (declared.len != observed.len) return error.IncompleteObservations;
566     for (declared) |requested| {
567         for (observed) |actual| {
568             if (requested.eql(actual)) break;
569         } else return error.UndeclaredObservation;
570     }
571 }
572 
573 test "revision records preserve negative reads, pipeline order and exact bytes" {
574     const allocator = std.testing.allocator;
575     const versions = [_]Version{.{ .name = "compiler", .version = 1 }};
576     const pipeline = [_]Version{
577         .{ .name = "b", .version = 2 },
578         .{ .name = "a", .version = 1 },
579     };
580     const inputs = Inputs{
581         .compiler_manifest = "owned build inputs",
582         .versions = &versions,
583         .pipeline = &pipeline,
584         .options = "expanded defaults",
585         .policy = "strict",
586         .facts = &.{.{ .key = "absent", .value = null }},
587     };
588     const first = try encodeInputs(allocator, inputs, &.{}, "gates");
589     defer allocator.free(first);
590     var changed = inputs;
591     changed.facts = &.{.{ .key = "absent", .value = "" }};
592     const second = try encodeInputs(allocator, changed, &.{}, "gates");
593     defer allocator.free(second);
594     try std.testing.expect(!std.mem.eql(u8, first, second));
595     changed = inputs;
596     const reversed = [_]Version{ pipeline[1], pipeline[0] };
597     changed.pipeline = &reversed;
598     const third = try encodeInputs(allocator, changed, &.{}, "gates");
599     defer allocator.free(third);
600     try std.testing.expect(!std.mem.eql(u8, first, third));
601     const exact = Exact{ .address = "address", .inputs = first, .image = "image" };
602     const bytes = try encodeExact(allocator, exact);
603     defer allocator.free(bytes);
604     try std.testing.expect(exact.eql(try decodeExact(bytes)));
605     bytes[4] = 99;
606     try std.testing.expectError(error.UnknownRequiredField, decodeExact(bytes));
607 }
608 
609 test "revision records refuse undeclared observations and noncanonical maps" {
610     try std.testing.expectError(error.UndeclaredObservation, validateObservations(
611         &.{.{ .key = "capability", .value = null }},
612         &.{.{ .key = "capability", .value = "present" }},
613     ));
614     var writer = Writer.init(std.testing.allocator);
615     defer writer.deinit();
616     try std.testing.expectError(error.NoncanonicalOrder, writeFacts(&writer, &.{
617         .{ .key = "z", .value = null },
618         .{ .key = "a", .value = "value" },
619     }));
620 }
621 
622 test "revision records sort map keys while observations retain negative values" {
623     const allocator = std.testing.allocator;
624     const facts = [_]Fact{
625         .{ .key = "z", .value = null },
626         .{ .key = "a", .value = "" },
627     };
628     const versions = [_]Version{
629         .{ .name = "z", .version = 2 },
630         .{ .name = "a", .version = 1 },
631     };
632     var inputs = Inputs{
633         .compiler_manifest = "compiled inputs",
634         .versions = &versions,
635         .pipeline = &.{},
636         .options = "defaults",
637         .policy = "strict",
638         .facts = &facts,
639     };
640     const first = try encodeInputs(allocator, inputs, &.{}, "gates");
641     defer allocator.free(first);
642     const reversed_facts = [_]Fact{ facts[1], facts[0] };
643     const reversed_versions = [_]Version{ versions[1], versions[0] };
644     inputs.facts = &reversed_facts;
645     inputs.versions = &reversed_versions;
646     const second = try encodeInputs(allocator, inputs, &.{}, "gates");
647     defer allocator.free(second);
648     try std.testing.expectEqualSlices(u8, first, second);
649     try validateObservations(&facts, &reversed_facts);
650     try std.testing.expectError(error.UndeclaredObservation, validateObservations(
651         &facts,
652         &.{ facts[0], facts[0] },
653     ));
654     inputs.facts = &.{ facts[0], facts[0] };
655     try std.testing.expectError(error.NoncanonicalOrder, encodeInputs(
656         allocator,
657         inputs,
658         &.{},
659         "gates",
660     ));
661 }
662 
663 test "revision records semantic projections preserve float bits and tagged choices" {
664     const Choice = union(enum) { absent, threshold: u32 };
665     const Options = struct { value: f64, choice: Choice, sequence: [2]u16, note: ?[]const u8 };
666     var first = Writer.init(std.testing.allocator);
667     defer first.deinit();
668     var second = Writer.init(std.testing.allocator);
669     defer second.deinit();
670     var options = Options{
671         .value = @bitCast(@as(u64, 0x7ff8000000000042)),
672         .choice = .{ .threshold = 11 },
673         .sequence = .{ 2, 1 },
674         .note = null,
675     };
676     try writeValue(&first, options);
677     try writeValue(&second, options);
678     try std.testing.expectEqualSlices(u8, first.bytes.items, second.bytes.items);
679     second.bytes.clearRetainingCapacity();
680     options.value = @bitCast(@as(u64, 0x7ff8000000000043));
681     try writeValue(&second, options);
682     try std.testing.expect(!std.mem.eql(u8, first.bytes.items, second.bytes.items));
683     second.bytes.clearRetainingCapacity();
684     options.value = @bitCast(@as(u64, 0x7ff8000000000042));
685     options.choice = .absent;
686     try writeValue(&second, options);
687     try std.testing.expect(!std.mem.eql(u8, first.bytes.items, second.bytes.items));
688 }
689 
690 test "revision records expose exact address and input closure without mutable owners" {
691     const allocator = std.testing.allocator;
692     const address = Address{
693         .producer = "compiler",
694         .source = "entry",
695         .stage = "ir",
696         .variant = "a",
697     };
698     const encoded_address = try encodeAddress(allocator, address);
699     defer allocator.free(encoded_address);
700     try std.testing.expect(address.eql(try decodeAddress(encoded_address)));
701     const dependency = try encodeExact(allocator, .{
702         .address = encoded_address,
703         .inputs = "input",
704         .image = "image",
705     });
706     defer allocator.free(dependency);
707     const inputs = try encodeInputs(allocator, .{
708         .compiler_manifest = "compiled inputs",
709         .versions = &.{},
710         .pipeline = &.{.{ .name = "pass", .version = 2 }},
711         .options = "defaults",
712         .policy = "strict",
713         .facts = &.{.{ .key = "absent", .value = null }},
714     }, &.{.{ .role = "source", .exact = dependency }}, "gates");
715     defer allocator.free(inputs);
716     const view = try decodeInputs(inputs);
717     var pipeline = view.pipeline.iterator();
718     try std.testing.expectEqualStrings("pass", (try pipeline.next()).?.name);
719     try std.testing.expectEqual(null, try pipeline.next());
720     var facts = view.facts.iterator();
721     try std.testing.expectEqual(null, (try facts.next()).?.value);
722     var dependencies = view.dependencies.iterator();
723     try std.testing.expectEqualSlices(u8, dependency, (try dependencies.next()).?.exact);
724     try std.testing.expectEqual(null, try dependencies.next());
725 }
726 
727 test "revision records encode into reserved capacity without changing exact bytes" {
728     const exact = Exact{
729         .address = "address",
730         .inputs = "exact inputs",
731         .image = "canonical image",
732     };
733     const expected = try encodeExact(std.testing.allocator, exact);
734     defer std.testing.allocator.free(expected);
735     var storage: [256]u8 = undefined;
736     const actual = try encodeExactInto(storage[0..expected.len], exact);
737     try std.testing.expect(@intFromPtr(actual.ptr) == @intFromPtr(&storage));
738     try std.testing.expectEqualSlices(u8, expected, actual);
739     try std.testing.expectEqual(expected.len, try exactSize(
740         exact.address.len,
741         exact.inputs.len,
742         exact.image.len,
743     ));
744     @memset(&storage, 0xa5);
745     try std.testing.expectError(error.RecordLimit, encodeExactInto(
746         storage[0 .. expected.len - 1],
747         exact,
748     ));
749     for (storage) |byte| try std.testing.expectEqual(@as(u8, 0xa5), byte);
750     try std.testing.expectError(error.RecordOverflow, exactSize(std.math.maxInt(usize), 1, 0));
751 }
752 
753 test "revision records bound semantic projection depth at the exact limit" {
754     const AtLimit = comptime blk: {
755         var T: type = u16;
756         for (1..maximum_projection_depth) |_| T = [1]T;
757         break :blk T;
758     };
759     var writer = Writer.init(std.testing.allocator);
760     defer writer.deinit();
761     try writeValue(&writer, std.mem.zeroes(AtLimit));
762     try std.testing.expect(writer.bytes.items.len > 0);
763     writer.bytes.clearRetainingCapacity();
764     try std.testing.expectError(error.UnencodableProduct, writeValue(
765         &writer,
766         std.mem.zeroes([1]AtLimit),
767     ));
768 }
769 
770 test "revision records reject cyclic semantic projections within bounded depth" {
771     const Node = struct { children: []const @This() };
772     var nodes: [1]Node = undefined;
773     nodes[0] = .{ .children = &nodes };
774     var writer = Writer.init(std.testing.allocator);
775     defer writer.deinit();
776     try std.testing.expectError(error.UnencodableProduct, writeValue(&writer, nodes[0]));
777 }