@@ -37,12 +37,13 @@ pub const Result = enum(c_int) {
3737 null_pointer = 4 ,
3838};
3939
40- /// Library handle (opaque to prevent direct access)
41- pub const Handle = opaque {
42- // Internal state hidden from C
40+ /// Library handle. A plain struct on the Zig side; C consumers only ever
41+ /// hold it behind `?*Handle` and never dereference it, so it stays opaque
42+ /// across the ABI. (The template declared this `opaque` *with fields*,
43+ /// which is a compile error — fixed as part of de-stubbing for #6.)
44+ pub const Handle = struct {
4345 allocator : std.mem.Allocator ,
4446 initialized : bool ,
45- // Add your fields here
4647};
4748
4849//==============================================================================
@@ -209,7 +210,7 @@ export fn verisimdb_data_build_info() [*:0]const u8 {
209210//==============================================================================
210211
211212/// Callback function type (C ABI)
212- pub const Callback = * const fn (u64 , u32 ) callconv (.C ) u32 ;
213+ pub const Callback = * const fn (u64 , u32 ) callconv (.c ) u32 ;
213214
214215/// Register a callback
215216export fn verisimdb_data_register_callback (
@@ -248,6 +249,100 @@ export fn verisimdb_data_is_initialized(handle: ?*Handle) u32 {
248249 return if (h .initialized ) 1 else 0 ;
249250}
250251
252+ //==============================================================================
253+ // Octad ABI (#6, V-L3-N1)
254+ //
255+ // The verisimdb octad has eight dimensions. `OctadDimension` carries the
256+ // per-dimension presence byte; `ProvenanceEntry` mirrors the octad
257+ // `provenance` block (a 64-bit content hash plus NUL-padded tool/version
258+ // identifiers). Both are `extern struct` so the C ABI / layout is stable
259+ // across the FFI boundary, and each has a wire encode/decode pair so a
260+ // consumer can prove a lossless round-trip.
261+ //==============================================================================
262+
263+ /// Eight octad dimension presence bytes (0 = absent, 1 = present).
264+ pub const OctadDimension = extern struct {
265+ data : u8 ,
266+ metadata : u8 ,
267+ provenance : u8 ,
268+ lineage : u8 ,
269+ constraints : u8 ,
270+ access_control : u8 ,
271+ temporal : u8 ,
272+ simulation : u8 ,
273+ };
274+
275+ pub const OCTAD_WIRE_LEN : usize = 8 ;
276+
277+ /// Serialize an `OctadDimension` (8 bytes, dimension order). Returns the
278+ /// number of bytes written, or -1 on a null/short-buffer error.
279+ export fn verisimdb_data_octad_encode (
280+ in : ? * const OctadDimension ,
281+ out : ? [* ]u8 ,
282+ cap : usize ,
283+ ) isize {
284+ const d = in orelse return -1 ;
285+ const o = out orelse return -1 ;
286+ if (cap < OCTAD_WIRE_LEN ) return -1 ;
287+ const src = std .mem .asBytes (d );
288+ @memcpy (o [0.. OCTAD_WIRE_LEN ], src [0.. OCTAD_WIRE_LEN ]);
289+ return @intCast (OCTAD_WIRE_LEN );
290+ }
291+
292+ /// Deserialize an `OctadDimension` from `buf`.
293+ export fn verisimdb_data_octad_decode (
294+ buf : ? [* ]const u8 ,
295+ len : usize ,
296+ out : ? * OctadDimension ,
297+ ) Result {
298+ const b = buf orelse return .null_pointer ;
299+ const o = out orelse return .null_pointer ;
300+ if (len < OCTAD_WIRE_LEN ) return .invalid_param ;
301+ const dst = std .mem .asBytes (o );
302+ @memcpy (dst [0.. OCTAD_WIRE_LEN ], b [0.. OCTAD_WIRE_LEN ]);
303+ return .ok ;
304+ }
305+
306+ /// One provenance record: a content hash plus NUL-padded identifiers.
307+ pub const ProvenanceEntry = extern struct {
308+ hash : u64 ,
309+ tool : [32 ]u8 ,
310+ version : [16 ]u8 ,
311+ };
312+
313+ /// hash (8, little-endian) + tool (32) + version (16).
314+ pub const PROVENANCE_WIRE_LEN : usize = 8 + 32 + 16 ;
315+
316+ /// Serialize a `ProvenanceEntry`. Returns bytes written or -1.
317+ export fn verisimdb_data_provenance_encode (
318+ in : ? * const ProvenanceEntry ,
319+ out : ? [* ]u8 ,
320+ cap : usize ,
321+ ) isize {
322+ const e = in orelse return -1 ;
323+ const o = out orelse return -1 ;
324+ if (cap < PROVENANCE_WIRE_LEN ) return -1 ;
325+ std .mem .writeInt (u64 , o [0.. 8], e .hash , .little );
326+ @memcpy (o [8.. 40], & e .tool );
327+ @memcpy (o [40.. 56], & e .version );
328+ return @intCast (PROVENANCE_WIRE_LEN );
329+ }
330+
331+ /// Deserialize a `ProvenanceEntry` from `buf`.
332+ export fn verisimdb_data_provenance_decode (
333+ buf : ? [* ]const u8 ,
334+ len : usize ,
335+ out : ? * ProvenanceEntry ,
336+ ) Result {
337+ const b = buf orelse return .null_pointer ;
338+ const e = out orelse return .null_pointer ;
339+ if (len < PROVENANCE_WIRE_LEN ) return .invalid_param ;
340+ e .hash = std .mem .readInt (u64 , b [0.. 8], .little );
341+ @memcpy (& e .tool , b [8.. 40]);
342+ @memcpy (& e .version , b [40.. 56]);
343+ return .ok ;
344+ }
345+
251346//==============================================================================
252347// Tests
253348//==============================================================================
@@ -272,3 +367,58 @@ test "version" {
272367 const ver_str = std .mem .span (ver );
273368 try std .testing .expectEqualStrings (VERSION , ver_str );
274369}
370+
371+ test "octad dimension round-trip is lossless" {
372+ const in = OctadDimension {
373+ .data = 1 ,
374+ .metadata = 1 ,
375+ .provenance = 1 ,
376+ .lineage = 0 ,
377+ .constraints = 1 ,
378+ .access_control = 0 ,
379+ .temporal = 1 ,
380+ .simulation = 0 ,
381+ };
382+ var buf : [OCTAD_WIRE_LEN ]u8 = undefined ;
383+ const n = verisimdb_data_octad_encode (& in , & buf , buf .len );
384+ try std .testing .expectEqual (@as (isize , @intCast (OCTAD_WIRE_LEN )), n );
385+
386+ var out : OctadDimension = undefined ;
387+ try std .testing .expectEqual (Result .ok , verisimdb_data_octad_decode (& buf , buf .len , & out ));
388+ try std .testing .expectEqual (in , out );
389+ }
390+
391+ test "octad encode rejects a short buffer" {
392+ const in = std .mem .zeroes (OctadDimension );
393+ var small : [3 ]u8 = undefined ;
394+ try std .testing .expectEqual (@as (isize , -1 ), verisimdb_data_octad_encode (& in , & small , small .len ));
395+ }
396+
397+ test "provenance entry round-trip is lossless" {
398+ var in = std .mem .zeroes (ProvenanceEntry );
399+ in .hash = 0xDEAD_BEEF_CAFE_F00D ;
400+ @memcpy (in .tool [0.. 7], "verisim" );
401+ @memcpy (in .version [0.. 5], "0.1.0" );
402+
403+ var buf : [PROVENANCE_WIRE_LEN ]u8 = undefined ;
404+ const n = verisimdb_data_provenance_encode (& in , & buf , buf .len );
405+ try std .testing .expectEqual (@as (isize , @intCast (PROVENANCE_WIRE_LEN )), n );
406+
407+ var out : ProvenanceEntry = undefined ;
408+ try std .testing .expectEqual (
409+ Result .ok ,
410+ verisimdb_data_provenance_decode (& buf , buf .len , & out ),
411+ );
412+ try std .testing .expectEqual (in .hash , out .hash );
413+ try std .testing .expectEqualSlices (u8 , & in .tool , & out .tool );
414+ try std .testing .expectEqualSlices (u8 , & in .version , & out .version );
415+ }
416+
417+ test "provenance decode rejects a short buffer" {
418+ var out : ProvenanceEntry = undefined ;
419+ var short : [10 ]u8 = undefined ;
420+ try std .testing .expectEqual (
421+ Result .invalid_param ,
422+ verisimdb_data_provenance_decode (& short , short .len , & out ),
423+ );
424+ }
0 commit comments