Skip to content

Commit 403cd0d

Browse files
hyperpolymathclaude
andcommitted
feat: add VeriSimDB Zig FFI module for health zone persistence
Add verisimdb.zig with persistReading() and makeReadingId() for collection session-sentinel:health. Health zone readings are persisted to VeriSimDB REST for cross-reboot trend analysis by Hypatia. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 052c9be commit 403cd0d

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// session-sentinel :: src/interface/ffi/src/verisimdb.zig
5+
//
6+
// VeriSimDB persistence client for health zone history and scan records.
7+
//
8+
// Persists session-sentinel health readings to VeriSimDB under the collection
9+
// `session-sentinel:health` so that Hypatia rules can detect degradation
10+
// trends across reboots and gitbot-fleet can act on Purple-zone breaches.
11+
//
12+
// ## Collection schema (session-sentinel:health)
13+
//
14+
// ```json
15+
// {
16+
// "reading_id": "ss:1740000000000:abc123",
17+
// "timestamp": "2026-01-30T12:00:00Z",
18+
// "bytes_used": 524288000,
19+
// "health_zone": "Yellow",
20+
// "trend": "degrading",
21+
// "flash_check": false,
22+
// "hostname": "hydra"
23+
// }
24+
// ```
25+
//
26+
// ## Environment
27+
//
28+
// Set `VERISIMDB_URL` to override the default `http://localhost:8080`.
29+
//
30+
// ## Fail-open semantics
31+
//
32+
// VeriSimDB is best-effort. Health readings are logged locally first;
33+
// VeriSimDB persistence is additional durable storage for cross-reboot
34+
// analysis. If VeriSimDB is unreachable, `persistReading` returns an error
35+
// and the caller continues with local-only storage.
36+
37+
const std = @import("std");
38+
39+
// ---------------------------------------------------------------------------
40+
// Constants
41+
// ---------------------------------------------------------------------------
42+
43+
const DEFAULT_URL = "http://localhost:8080";
44+
const COLLECTION = "session-sentinel:health";
45+
46+
// ---------------------------------------------------------------------------
47+
// Public types
48+
// ---------------------------------------------------------------------------
49+
50+
/// Health zones matching the Green/Yellow/Red/Purple classification in health.as.
51+
pub const HealthZone = enum {
52+
Green,
53+
Yellow,
54+
Red,
55+
Purple,
56+
};
57+
58+
/// Trend direction from health.as trend analysis.
59+
pub const Trend = enum {
60+
improving,
61+
stable,
62+
degrading,
63+
};
64+
65+
/// A single health reading for VeriSimDB persistence.
66+
pub const HealthReading = struct {
67+
reading_id: []const u8,
68+
timestamp: []const u8,
69+
bytes_used: u64,
70+
zone: HealthZone,
71+
trend: Trend,
72+
flash_check: bool,
73+
hostname: []const u8,
74+
};
75+
76+
// ---------------------------------------------------------------------------
77+
// Public API
78+
// ---------------------------------------------------------------------------
79+
80+
/// Persist a health reading to VeriSimDB (collection: session-sentinel:health).
81+
///
82+
/// Uses HTTP PUT to `/v1/session-sentinel:health/<reading_id>`.
83+
/// Returns an error on connectivity failure — caller should log and continue.
84+
pub fn persistReading(allocator: std.mem.Allocator, reading: HealthReading) !void {
85+
const base_url = std.posix.getenv("VERISIMDB_URL") orelse DEFAULT_URL;
86+
87+
const url = try std.fmt.allocPrint(allocator, "{s}/v1/{s}/{s}", .{
88+
base_url, COLLECTION, reading.reading_id,
89+
});
90+
defer allocator.free(url);
91+
92+
const zone_str = @tagName(reading.zone);
93+
const trend_str = @tagName(reading.trend);
94+
const flash_str = if (reading.flash_check) "true" else "false";
95+
96+
const body = try std.fmt.allocPrint(allocator,
97+
\\{{
98+
\\ "reading_id": "{s}",
99+
\\ "timestamp": "{s}",
100+
\\ "bytes_used": {d},
101+
\\ "health_zone": "{s}",
102+
\\ "trend": "{s}",
103+
\\ "flash_check": {s},
104+
\\ "hostname": "{s}"
105+
\\}}
106+
, .{
107+
reading.reading_id,
108+
reading.timestamp,
109+
reading.bytes_used,
110+
zone_str,
111+
trend_str,
112+
flash_str,
113+
reading.hostname,
114+
});
115+
defer allocator.free(body);
116+
117+
try httpPut(allocator, url, body);
118+
}
119+
120+
/// Generate a stable reading ID from hostname and timestamp milliseconds.
121+
///
122+
/// Format: `ss:<ts_ms>:<first_8_of_hostname_sha256_hex>`
123+
pub fn makeReadingId(allocator: std.mem.Allocator, hostname: []const u8, ts_ms: u64) ![]u8 {
124+
var hash_buf: [32]u8 = undefined;
125+
std.crypto.hash.sha2.Sha256.hash(hostname, &hash_buf, .{});
126+
const hex = try std.fmt.allocPrint(allocator, "{}", .{std.fmt.fmtSliceHexLower(hash_buf[0..4])});
127+
defer allocator.free(hex);
128+
return std.fmt.allocPrint(allocator, "ss:{d}:{s}", .{ ts_ms, hex });
129+
}
130+
131+
// ---------------------------------------------------------------------------
132+
// Internal helpers
133+
// ---------------------------------------------------------------------------
134+
135+
fn httpPut(allocator: std.mem.Allocator, url: []const u8, body: []const u8) !void {
136+
var client = std.http.Client{ .allocator = allocator };
137+
defer client.deinit();
138+
139+
const uri = try std.Uri.parse(url);
140+
141+
var header_buf: [4096]u8 = undefined;
142+
var req = try client.open(.PUT, uri, .{
143+
.server_header_buffer = &header_buf,
144+
.extra_headers = &.{
145+
.{ .name = "Content-Type", .value = "application/json" },
146+
},
147+
});
148+
defer req.deinit();
149+
150+
req.transfer_encoding = .{ .content_length = body.len };
151+
try req.send();
152+
try req.writeAll(body);
153+
try req.finish();
154+
try req.wait();
155+
156+
const status = req.response.status;
157+
if (@intFromEnum(status) < 200 or @intFromEnum(status) >= 300) {
158+
return error.VeriSimDbHttpError;
159+
}
160+
}

0 commit comments

Comments
 (0)