Luigit
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);
}