repositories / termux-janitor
termux-janitor
Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.
owned by admin
tools/spec-engine/src/diag.zig
Raw//! Actionable diagnostics.
//!
//! Every engine failure names a stable code, the canonical owner of the
//! disputed fact, the commands that inspect it, and the permitted resolution.
//! Nothing in this file assumes product knowledge; repository-specific text
//! arrives through the `profile` module.
const std = @import("std");
const model_types = @import("model.zig");
const profile = @import("profile");
const tool = profile.tool_invocation;
/// The canonical owner of a disputed fact, plus how to inspect and resolve it.
pub const Subject = struct {
/// Stable, greppable diagnostic code.
code: []const u8,
/// What kind of fact is inconsistent.
kind: []const u8,
/// Canonical owner of the fact.
owner: []const u8,
/// Commands that inspect the fact and its dependents.
inspect: []const []const u8 = &.{},
/// The permitted resolution, stated as an instruction to the author.
resolve: []const u8,
};
pub const subject = struct {
pub const document: Subject = .{
.code = "TJSP-E010",
.kind = "document catalog",
.owner = profile.paths.documents,
.inspect = &.{tool ++ " query document NAME"},
.resolve = "Correct the catalog entry, then regenerate projections with " ++
profile.generate_command ++ ".",
};
pub const concept: Subject = .{
.code = "TJSP-E020",
.kind = "concept",
.owner = profile.paths.concepts,
.inspect = &.{ tool ++ " query concept ID", tool ++ " query owner OWNER" },
.resolve = "Correct the concept in its registry, then regenerate projections with " ++
profile.generate_command ++ ".",
};
pub const requirement: Subject = .{
.code = "TJSP-E110",
.kind = "requirement",
.owner = profile.paths.requirements,
.inspect = &.{ tool ++ " query requirement ID", tool ++ " status", tool ++ " impact ID" },
.resolve = "Correct the requirement record, or revise the owning document it points at.",
};
pub const obligation: Subject = .{
.code = "TJSP-E310",
.kind = "verification obligation",
.owner = profile.paths.verification,
.inspect = &.{ tool ++ " query obligation ID", tool ++ " status", tool ++ " impact ID" },
.resolve = "Correct the obligation, or revise the requirement revision it verifies.",
};
pub const test_spec: Subject = .{
.code = "TJSP-E210",
.kind = "test specification",
.owner = profile.paths.test_specs,
.inspect = &.{ tool ++ " query test-spec ID", tool ++ " status", tool ++ " impact ID" },
.resolve = "Correct the test specification, or scaffold a consistent one with " ++
tool ++ " new-test NAME.",
};
pub const fixture: Subject = .{
.code = "TJSP-E220",
.kind = "prose fixture",
.owner = profile.paths.fixture_root,
.inspect = &.{ tool ++ " query test-spec ID", tool ++ " status", tool ++ " impact FIXTURE-PATH" },
.resolve = "Correct the fixture prose or its instrumentation so that both express the " ++
"same declared oracles.",
};
pub const binding: Subject = .{
.code = "TJSP-E240",
.kind = "executable binding",
.owner = profile.paths.test_bindings,
.inspect = &.{ tool ++ " query test-spec ID", tool ++ " status", tool ++ " impact BINDING-PATH" },
.resolve = "Correct the binding registration, or produce a fresh implementation with " ++
tool ++ " scaffold-test TEST-ID.",
};
pub const implementation: Subject = .{
.code = "TJSP-E400",
.kind = "implementation unit",
.owner = profile.paths.implementations,
.inspect = &.{ tool ++ " query implementation ID", tool ++ " status", tool ++ " impact SOURCE-PATH" },
.resolve = "Register the source with exactly one implementation unit " ++
"and the obligations it realizes.",
};
pub const unregistered_implementation: Subject = .{
.code = "TJSP-E420",
.kind = "unregistered implementation source",
.owner = profile.paths.implementations,
.inspect = &.{tool ++ " status"},
.resolve = "Register the source with the obligations it realizes:\n" ++
" " ++ tool ++ " new-source PATH OBLIGATION-ID...",
};
pub const unregistered_binding: Subject = .{
.code = "TJSP-E251",
.kind = "unregistered fixture implementation",
.owner = profile.paths.test_bindings,
.inspect = &.{tool ++ " status"},
.resolve = "Register the binding for its instrumented fixture, or delete the source. " ++
"A plan is printed by\n " ++ tool ++ " new-test NAME REQUIREMENT SUITE.",
};
pub const product_test: Subject = .{
.code = "TJSP-E250",
.kind = "executable product test",
.owner = profile.paths.test_specs,
.inspect = &.{tool ++ " status"},
.resolve = "Move the assertions into a prose fixture and its instrumentation block:\n" ++
" " ++ tool ++ " new-test NAME REQUIREMENT SUITE",
};
pub const projection: Subject = .{
.code = "TJSP-E500",
.kind = "generated projection",
.owner = "generated output",
.inspect = &.{},
.resolve = "Regenerate projections with " ++ profile.generate_command ++
"; never edit generated files by hand.",
};
pub const path: Subject = .{
.code = "TJSP-E600",
.kind = "repository path",
.owner = "repository layout",
.inspect = &.{},
.resolve = "Reference an existing repository-relative path without parent components.",
};
};
/// Report an inconsistency with its owner, inspection commands, and resolution.
pub fn fail(
writer: *std.Io.Writer,
context: Subject,
comptime format: []const u8,
args: anytype,
) !void {
try writer.print("{s}: " ++ format ++ "\n\n", .{context.code} ++ args);
try writer.print("kind: {s}\nowner: {s}\n", .{ context.kind, context.owner });
for (context.inspect, 0..) |command, index| {
const label = if (index == 0) "inspect: " else " ";
try writer.print("{s}{s}\n", .{ label, command });
}
try writer.print("resolve: {s}\n", .{context.resolve});
try writer.print("verify: {s}\n", .{profile.verify_command});
return error.InvalidSpecification;
}
pub const FingerprintDomain = enum { fixture, binding, implementation };
pub fn checkFingerprint(
source: []const u8,
expected: []const u8,
domain: FingerprintDomain,
id: []const u8,
file_path: []const u8,
stderr: *std.Io.Writer,
) !void {
const context = switch (domain) {
.fixture => subject.fixture,
.binding => subject.binding,
.implementation => subject.implementation,
};
if (!sha256Valid(expected)) {
return fail(stderr, context, "invalid SHA-256 fingerprint on {s}: {s}", .{ id, expected });
}
var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(source, &digest, .{});
var actual_buffer: [digest.len * 2]u8 = undefined;
const actual = std.fmt.bufPrint(&actual_buffer, "{x}", .{&digest}) catch unreachable;
if (std.mem.eql(u8, expected, actual)) return;
const code = switch (domain) {
.fixture => "TJSP-E230",
.binding => "TJSP-E260",
.implementation => "TJSP-E410",
};
try stderr.print(
"{s}: reviewed {s} changed without synchronization\n\n" ++
"owner: {s}\npath: {s}\nexpected: {s}\nactual: {s}\n\n" ++
"inspect: {s} impact {s}\n" ++
" {s} fingerprint {s}\n" ++
"resolve: classify the change. Presentation-only changes keep the revision and " ++
"record the current fingerprint.\n" ++
" Semantic changes increment the owning revision and migrate every " ++
"dependent link.\n" ++
"verify: {s}\n",
.{
code,
@tagName(domain),
id,
file_path,
expected,
actual,
tool,
id,
tool,
file_path,
profile.verify_command,
},
);
return error.InvalidSpecification;
}
pub fn sha256Valid(value: []const u8) bool {
if (value.len != std.crypto.hash.sha2.Sha256.digest_length * 2) return false;
for (value) |byte| {
if (!std.ascii.isDigit(byte) and !(byte >= 'a' and byte <= 'f')) return false;
}
return true;
}
pub fn staleRequirement(
stderr: *std.Io.Writer,
reference: model_types.RequirementReference,
current_revision: u32,
dependent: []const u8,
) !void {
try stderr.print(
"TJSP-E120: stale requirement reference\n\n" ++
"dependent: {s}\nreference: {s}@{d}\ncurrent: {s}@{d}\n\n" ++
"inspect: {s} query requirement {s}\n" ++
" {s} impact {s}\n" ++
"resolve: read the current requirement, decide whether the dependent still holds, " ++
"then migrate or remove the link.\n" ++
" Never advance the referenced revision mechanically.\n" ++
"verify: {s}\n",
.{
dependent,
reference.id,
reference.revision,
reference.id,
current_revision,
tool,
reference.id,
tool,
reference.id,
profile.verify_command,
},
);
return error.InvalidSpecification;
}
pub fn staleObligation(
stderr: *std.Io.Writer,
reference: model_types.ObligationReference,
current_revision: u32,
dependent: []const u8,
) !void {
try stderr.print(
"TJSP-E320: stale verification-obligation reference\n\n" ++
"dependent: {s}\nreference: {s}@{d}\ncurrent: {s}@{d}\n\n" ++
"inspect: {s} query obligation {s}\n" ++
" {s} impact {s}\n" ++
"resolve: decide whether the dependent still verifies the revised obligation, " ++
"then migrate or remove the link.\n" ++
"verify: {s}\n",
.{
dependent,
reference.id,
reference.revision,
reference.id,
current_revision,
tool,
reference.id,
tool,
reference.id,
profile.verify_command,
},
);
return error.InvalidSpecification;
}
pub fn validateRepositoryPath(io: std.Io, file_path: []const u8, stderr: *std.Io.Writer) !void {
if (std.fs.path.isAbsolute(file_path)) {
return fail(stderr, subject.path, "absolute repository path: {s}", .{file_path});
}
if (std.mem.indexOf(u8, file_path, "..") != null) {
return fail(stderr, subject.path, "parent component in repository path: {s}", .{file_path});
}
std.Io.Dir.cwd().access(io, file_path, .{}) catch {
return fail(stderr, subject.path, "missing repository path: {s}", .{file_path});
};
}
test "diagnostics name the owner, an inspection command, and a resolution" {
var output = std.Io.Writer.Allocating.init(std.testing.allocator);
defer output.deinit();
try std.testing.expectError(
error.InvalidSpecification,
fail(&output.writer, subject.test_spec, "duplicate test-spec ID: {s}", .{"test-example"}),
);
const written = output.written();
try std.testing.expect(std.mem.indexOf(u8, written, "TJSP-E210") != null);
try std.testing.expect(std.mem.indexOf(u8, written, "test-example") != null);
try std.testing.expect(std.mem.indexOf(u8, written, "owner:") != null);
try std.testing.expect(std.mem.indexOf(u8, written, "inspect:") != null);
try std.testing.expect(std.mem.indexOf(u8, written, "status") != null);
try std.testing.expect(std.mem.indexOf(u8, written, "resolve:") != null);
try std.testing.expect(std.mem.indexOf(u8, written, "verify:") != null);
}
test "strict fingerprints report actionable drift" {
var output = std.Io.Writer.Allocating.init(std.testing.allocator);
defer output.deinit();
try checkFingerprint(
"abc",
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
.fixture,
"fixture-example",
"fixtures/example.md",
&output.writer,
);
try std.testing.expectError(
error.InvalidSpecification,
checkFingerprint(
"changed",
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
.fixture,
"fixture-example",
"fixtures/example.md",
&output.writer,
),
);
try std.testing.expect(std.mem.indexOf(u8, output.written(), "TJSP-E230") != null);
try std.testing.expect(std.mem.indexOf(u8, output.written(), "impact fixture-example") != null);
}