repositories / termux-janitor
termux-janitor
Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.
owned by admin
tools/spec-engine/src/sync.zig
Raw//! Explicit, on-demand synchronization transformations.
//!
//! A transformation records that a change was reviewed. It never chooses a
//! semantic relationship, never advances a semantic revision, and never runs
//! from a read-only gate.
const std = @import("std");
const model_types = @import("model.zig");
const project = @import("project.zig");
const profile = @import("profile");
const digest_length = std.crypto.hash.sha2.Sha256.digest_length * 2;
pub const Change = struct {
registry: []const u8,
path: []const u8,
owner: []const u8,
previous: [digest_length]u8,
current: [digest_length]u8,
};
/// Rewrite every reviewed fingerprint that no longer matches its file.
///
/// `filters`, when non-empty, restricts the update to those tracked paths.
pub fn fingerprints(
allocator: std.mem.Allocator,
io: std.Io,
graph: *const model_types.Graph,
filters: []const []const u8,
writer: *std.Io.Writer,
) !u32 {
var updated: u32 = 0;
updated += try syncRegistry(allocator, io, profile.paths.test_specs, filters, writer, .{
.fixtures = graph.test_specs,
});
updated += try syncRegistry(allocator, io, profile.paths.test_bindings, filters, writer, .{
.bindings = graph.test_bindings,
});
updated += try syncRegistry(allocator, io, profile.paths.implementations, filters, writer, .{
.implementations = graph.implementations,
});
if (updated == 0) {
try writer.writeAll("every reviewed fingerprint already matches its file\n");
return 0;
}
try writer.print(
"\n{d} reviewed fingerprint(s) updated.\n" ++
"A fingerprint records review only. Semantic changes still need a revision " ++
"increment\nand migration of every dependent link.\n" ++
"verify: {s}\n",
.{ updated, profile.verify_command },
);
return updated;
}
const Entries = union(enum) {
fixtures: []const model_types.TestSpec,
bindings: []const model_types.TestBinding,
implementations: []const model_types.ImplementationUnit,
};
fn syncRegistry(
allocator: std.mem.Allocator,
io: std.Io,
registry: []const u8,
filters: []const []const u8,
writer: *std.Io.Writer,
entries: Entries,
) !u32 {
var source = try std.Io.Dir.cwd().readFileAlloc(
io,
registry,
allocator,
.limited(model_types.file_size_max),
);
defer allocator.free(source);
var updated: u32 = 0;
switch (entries) {
.fixtures => |items| for (items) |item| {
updated += try replaceDigest(
allocator,
io,
&source,
registry,
item.fixture.path,
item.fixture.sha256,
item.fixture.id,
filters,
writer,
);
},
.bindings => |items| for (items) |item| {
updated += try replaceDigest(
allocator,
io,
&source,
registry,
item.source,
item.source_sha256,
item.test_spec.id,
filters,
writer,
);
},
.implementations => |items| for (items) |item| {
for (item.sources) |reference| {
updated += try replaceDigest(
allocator,
io,
&source,
registry,
reference.path,
reference.sha256,
item.id,
filters,
writer,
);
}
},
}
if (updated > 0) {
var temporary_buffer: [256]u8 = undefined;
const temporary = try std.fmt.bufPrint(
&temporary_buffer,
"{s}.spec-tool.tmp",
.{registry},
);
try project.replaceFile(io, registry, temporary, source);
}
return updated;
}
fn replaceDigest(
allocator: std.mem.Allocator,
io: std.Io,
source: *[]u8,
registry: []const u8,
tracked_path: []const u8,
recorded: []const u8,
owner: []const u8,
filters: []const []const u8,
writer: *std.Io.Writer,
) !u32 {
if (filters.len > 0 and !pathSelected(filters, tracked_path)) return 0;
const bytes = try std.Io.Dir.cwd().readFileAlloc(
io,
tracked_path,
allocator,
.limited(model_types.file_size_max),
);
defer allocator.free(bytes);
var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
var current: [digest_length]u8 = undefined;
_ = std.fmt.bufPrint(¤t, "{x}", .{&digest}) catch unreachable;
if (std.mem.eql(u8, recorded, ¤t)) return 0;
if (recorded.len != digest_length) return error.InvalidRecordedFingerprint;
const position = std.mem.indexOf(u8, source.*, recorded) orelse
return error.RecordedFingerprintNotFound;
if (std.mem.indexOfPos(u8, source.*, position + recorded.len, recorded) != null) {
return error.AmbiguousRecordedFingerprint;
}
@memcpy(source.*[position..][0..digest_length], ¤t);
try writer.print("{s}\n registry: {s}\n owner: {s}\n {s} -> {s}\n", .{
tracked_path,
registry,
owner,
recorded,
current,
});
return 1;
}
fn pathSelected(filters: []const []const u8, tracked_path: []const u8) bool {
for (filters) |filter| {
if (std.mem.eql(u8, filter, tracked_path)) return true;
}
return false;
}
/// Render the implementation-unit stanza for a source that is not yet registered.
pub fn newSource(
allocator: std.mem.Allocator,
io: std.Io,
graph: *const model_types.Graph,
source_path: []const u8,
obligation_ids: []const []const u8,
writer: *std.Io.Writer,
) !void {
if (!std.mem.startsWith(u8, source_path, profile.paths.implementation_root)) {
return error.SourceOutsideImplementationRoot;
}
if (!std.mem.endsWith(u8, source_path, ".zig")) return error.SourceIsNotZig;
if (obligation_ids.len == 0) return error.MissingObligation;
for (graph.implementations) |unit| {
for (unit.sources) |reference| {
if (std.mem.eql(u8, reference.path, source_path)) return error.SourceAlreadyRegistered;
}
}
const bytes = try std.Io.Dir.cwd().readFileAlloc(
io,
source_path,
allocator,
.limited(model_types.file_size_max),
);
defer allocator.free(bytes);
var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
var current: [digest_length]u8 = undefined;
_ = std.fmt.bufPrint(¤t, "{x}", .{&digest}) catch unreachable;
var stem_buffer: [128]u8 = undefined;
const stem = try unitName(&stem_buffer, sourceStem(source_path));
try writer.print("Append to {s}\n\n", .{profile.paths.implementations});
try writer.writeAll("----------------------------------------------------------------------\n");
try writer.print(" .{{\n .id = \"implementation-{s}\",\n", .{stem});
try writer.writeAll(" .revision = 1,\n .sources = .{\n .{\n");
try writer.print(" .path = \"{s}\",\n", .{source_path});
try writer.print(" .sha256 = \"{s}\",\n", .{current});
try writer.writeAll(" },\n },\n .realizes = .{\n");
for (obligation_ids) |id| {
const obligation = model_types.findObligation(graph.verification, id) orelse
return error.ObligationNotFound;
try writer.print(" .{{ .id = \"{s}\", .revision = {d} }},\n", .{
obligation.id,
obligation.revision,
});
}
try writer.writeAll(" },\n },\n");
try writer.writeAll("----------------------------------------------------------------------\n");
try writer.print(
"\nOne source belongs to exactly one implementation unit. Merge the stanza into an\n" ++
"existing unit instead when that unit already realizes the same obligations.\n" ++
"verify: {s}\n",
.{profile.verify_command},
);
}
/// Implementation-unit identifiers use the hyphenated name grammar.
fn unitName(buffer: []u8, stem: []const u8) ![]const u8 {
if (stem.len > buffer.len) return error.SourceNameTooLong;
for (stem, 0..) |byte, index| buffer[index] = if (byte == '_') '-' else byte;
return buffer[0..stem.len];
}
fn sourceStem(source_path: []const u8) []const u8 {
const start = if (std.mem.lastIndexOfScalar(u8, source_path, '/')) |index|
index + 1
else
0;
const end = source_path.len - ".zig".len;
return source_path[start..end];
}
test "source stems drop directories, extensions, and underscores" {
try std.testing.expectEqualStrings("main", sourceStem("src/main.zig"));
try std.testing.expectEqualStrings("scan", sourceStem("src/filesystem/scan.zig"));
var buffer: [64]u8 = undefined;
try std.testing.expectEqualStrings(
"probe-unit",
try unitName(&buffer, sourceStem("src/probe_unit.zig")),
);
}
test "unregistered sources are rejected before a stanza is offered" {
const graph: model_types.Graph = .{
.documents = &.{},
.concepts = &.{},
.groups = &.{},
.test_specs = &.{},
.test_bindings = &.{},
.verification = &.{},
.implementations = &.{},
};
var output = std.Io.Writer.Allocating.init(std.testing.allocator);
defer output.deinit();
try std.testing.expectError(error.SourceIsNotZig, newSource(
std.testing.allocator,
undefined,
&graph,
profile.paths.implementation_root ++ "notes.md",
&.{"obligation-example"},
&output.writer,
));
try std.testing.expectError(error.MissingObligation, newSource(
std.testing.allocator,
undefined,
&graph,
profile.paths.implementation_root ++ "example.zig",
&.{},
&output.writer,
));
}