Luigit
repositories / termux-janitor

termux-janitor

Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.

owned by admin

tools/spec-engine/src/query.zig

Raw
const std = @import("std");
const model_types = @import("model.zig");
const diag = @import("diag.zig");
const project = @import("project.zig");
const text = @import("text.zig");

const findObligation = model_types.findObligation;
const findRequirement = model_types.findRequirement;
const findTestBinding = model_types.findTestBinding;
const findTestSpec = model_types.findTestSpec;

/// Answer a generic relationship query.
///
/// Product-domain queries are dispatched by the profile consumer before this
/// function is reached.
pub fn runQuery(
    model: *const model_types.Graph,
    args: []const []const u8,
    writer: *std.Io.Writer,
) !void {
    if (args.len < 2 or args.len > 3) return error.InvalidQuery;
    const json = args.len == 3 and std.mem.eql(u8, args[2], "--json");
    if (args.len == 3 and !json) return error.InvalidQuery;
    if (std.mem.eql(u8, args[0], "requirement")) {
        return queryRequirement(model, args[1], json, writer);
    }
    if (std.mem.eql(u8, args[0], "concept")) {
        return queryConcept(model, args[1], json, writer);
    }
    if (std.mem.eql(u8, args[0], "document")) {
        return queryDocument(model, args[1], json, writer);
    }
    if (std.mem.eql(u8, args[0], "owner")) {
        return queryOwner(model, args[1], json, writer);
    }
    if (std.mem.eql(u8, args[0], "test-spec")) {
        return queryTestSpec(model, args[1], json, writer);
    }
    if (std.mem.eql(u8, args[0], "obligation")) {
        return queryObligation(model, args[1], json, writer);
    }
    if (std.mem.eql(u8, args[0], "implementation")) {
        return queryImplementation(model, args[1], json, writer);
    }
    return error.UnknownQuery;
}

pub fn queryRequirement(
    model: *const model_types.Graph,
    id: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    for (model.groups) |group| {
        for (group.requirements) |requirement| {
            if (!std.mem.eql(u8, requirement.id, id)) continue;
            if (json) {
                try writer.print("{f}\n", .{std.json.fmt(requirement, .{})});
            } else {
                try writer.print("{s}@{d}: {s}\n", .{
                    requirement.id,
                    requirement.revision,
                    requirement.label,
                });
                try writer.print("owner: {s}\n", .{requirement.owner});
                try writer.writeAll("suites: ");
                try project.renderSuites(requirement.suites, writer);
                try writer.print(
                    "\nlimitation: {s}\n",
                    .{@tagName(requirement.limitation_status)},
                );
            }
            return;
        }
    }
    return error.RequirementNotFound;
}

pub fn queryConcept(
    model: *const model_types.Graph,
    key: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    for (model.concepts) |concept| {
        const matches = std.mem.eql(u8, concept.id, key) or
            std.ascii.eqlIgnoreCase(concept.term, key);
        if (!matches) continue;
        if (json) {
            try writer.print("{f}\n", .{std.json.fmt(concept, .{})});
        } else {
            try writer.print("{s}: {s}\nowner: {s}\ndefinition: ", .{
                concept.id,
                concept.term,
                concept.owner,
            });
            try text.writeCollapsedWhitespace(writer, concept.definition);
            try writer.writeAll("\ninvariants:\n");
            for (concept.invariants) |invariant| {
                try writer.writeAll("- ");
                try text.writeCollapsedWhitespace(writer, invariant);
                try writer.writeByte('\n');
            }
        }
        return;
    }
    return error.ConceptNotFound;
}

pub fn queryDocument(
    model: *const model_types.Graph,
    name: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    for (model.documents) |document| {
        if (!std.mem.eql(u8, document.name, name)) continue;
        if (json) {
            try writer.print("{f}\n", .{std.json.fmt(document, .{})});
        } else {
            try writer.print("{s}: {s}\n{s}\n", .{
                document.name,
                document.location,
                document.description,
            });
        }
        return;
    }
    return error.DocumentNotFound;
}

pub fn queryOwner(
    model: *const model_types.Graph,
    owner: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    var match_count: u32 = 0;
    if (json) try writer.writeByte('[');
    for (model.groups) |group| {
        for (group.requirements) |requirement| {
            if (!std.mem.eql(u8, requirement.owner, owner)) continue;
            if (json) {
                if (match_count > 0) try writer.writeByte(',');
                try writer.print("{f}", .{std.json.fmt(requirement.id, .{})});
            } else {
                try writer.print("{s}@{d}: {s}\n", .{
                    requirement.id,
                    requirement.revision,
                    requirement.label,
                });
            }
            match_count += 1;
        }
    }
    if (json) try writer.writeAll("]\n");
    if (match_count == 0) return error.OwnerNotFound;
}

pub fn queryTestSpec(
    model: *const model_types.Graph,
    id: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    const test_spec = findTestSpec(model.test_specs, id) orelse return error.TestSpecNotFound;
    if (json) {
        try writer.print("{f}\n", .{std.json.fmt(test_spec, .{})});
        return;
    }
    try writer.print("{s}@{d}\nsuite: {s}\nfixture: {s}@{d}\npath: {s}\n", .{
        test_spec.id,
        test_spec.revision,
        @tagName(test_spec.suite),
        test_spec.fixture.id,
        test_spec.fixture.revision,
        test_spec.fixture.path,
    });
    if (findTestBinding(model.test_bindings, test_spec.id)) |binding| {
        try writer.print("state: bound\nsource: {s}\n", .{binding.source});
    } else {
        try writer.writeAll("state: specified\n");
    }
    try writer.writeAll("claims:\n");
    for (test_spec.claims) |claim| {
        try writer.print("- {s} ({s})\n", .{ claim.oracle, @tagName(claim.kind) });
        for (claim.obligations) |obligation| {
            try writer.print("  - {s}@{d}\n", .{ obligation.id, obligation.revision });
        }
    }
}

pub fn queryObligation(
    model: *const model_types.Graph,
    id: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    const obligation = findObligation(model.verification, id) orelse
        return error.ObligationNotFound;
    if (json) return writer.print("{f}\n", .{std.json.fmt(obligation, .{})});
    try writer.print("{s}@{d}: {s}\nkind: {s}\nrequirement: {s}@{d}\nsuites:", .{
        obligation.id,
        obligation.revision,
        obligation.label,
        @tagName(obligation.kind),
        obligation.requirement.id,
        obligation.requirement.revision,
    });
    for (obligation.suites) |suite| try writer.print(" {s}", .{@tagName(suite)});
    try writer.writeByte('\n');
}

pub fn queryImplementation(
    model: *const model_types.Graph,
    id: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    for (model.implementations) |unit| {
        if (!std.mem.eql(u8, unit.id, id)) continue;
        if (json) return writer.print("{f}\n", .{std.json.fmt(unit, .{})});
        try writer.print("{s}@{d}\nsources:\n", .{ unit.id, unit.revision });
        for (unit.sources) |source| try writer.print("- {s}\n", .{source.path});
        try writer.writeAll("realizes:\n");
        for (unit.realizes) |reference| {
            try writer.print("- {s}@{d}\n", .{ reference.id, reference.revision });
        }
        return;
    }
    return error.ImplementationNotFound;
}

pub fn runFingerprint(
    allocator: std.mem.Allocator,
    io: std.Io,
    path: []const u8,
    writer: *std.Io.Writer,
    stderr: *std.Io.Writer,
) !void {
    try diag.validateRepositoryPath(io, path, stderr);
    const source = try std.Io.Dir.cwd().readFileAlloc(
        io,
        path,
        allocator,
        .limited(model_types.file_size_max),
    );
    defer allocator.free(source);
    var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(source, &digest, .{});
    try writer.print("{x}  {s}\n", .{ &digest, path });
}

pub fn runImpact(
    model: *const model_types.Graph,
    target: []const u8,
    json: bool,
    writer: *std.Io.Writer,
) !void {
    if (json) return runImpactJson(model, target, writer);
    var matched = false;
    for (model.groups) |group| {
        for (group.requirements) |requirement| {
            if (!std.mem.eql(u8, requirement.id, target)) continue;
            matched = true;
            try writer.print("requirement {s}@{d}\n  owner {s}\n", .{
                requirement.id,
                requirement.revision,
                requirement.owner,
            });
        }
    }
    for (model.verification) |obligation| {
        if (std.mem.eql(u8, obligation.id, target) or
            std.mem.eql(u8, obligation.requirement.id, target))
        {
            matched = true;
            try writer.print("obligation {s}@{d}\n  requirement {s}@{d}\n", .{
                obligation.id,
                obligation.revision,
                obligation.requirement.id,
                obligation.requirement.revision,
            });
        }
    }
    for (model.test_specs) |test_spec| {
        if (testSpecMatchesImpact(model, &test_spec, target)) {
            matched = true;
            try writer.print("test {s}@{d}\n  fixture {s}@{d}\n  path {s}\n", .{
                test_spec.id,
                test_spec.revision,
                test_spec.fixture.id,
                test_spec.fixture.revision,
                test_spec.fixture.path,
            });
        }
    }
    for (model.implementations) |unit| {
        if (implementationMatchesImpact(model, &unit, target)) {
            matched = true;
            try writer.print("implementation {s}@{d}\n", .{ unit.id, unit.revision });
            for (unit.sources) |source| try writer.print("  source {s}\n", .{source.path});
        }
    }
    if (!matched) return error.ImpactTargetNotFound;
}

fn runImpactJson(
    model: *const model_types.Graph,
    target: []const u8,
    writer: *std.Io.Writer,
) !void {
    var matched = false;
    try writer.print("{{\"target\":{f},\"requirements\":[", .{std.json.fmt(target, .{})});
    var count: u32 = 0;
    for (model.groups) |group| {
        for (group.requirements) |requirement| {
            if (!std.mem.eql(u8, requirement.id, target)) continue;
            if (count > 0) try writer.writeByte(',');
            try writer.print("{f}", .{std.json.fmt(requirement, .{})});
            count += 1;
            matched = true;
        }
    }
    try writer.writeAll("],\"obligations\":[");
    count = 0;
    for (model.verification) |obligation| {
        if (!std.mem.eql(u8, obligation.id, target) and
            !std.mem.eql(u8, obligation.requirement.id, target)) continue;
        if (count > 0) try writer.writeByte(',');
        try writer.print("{f}", .{std.json.fmt(obligation, .{})});
        count += 1;
        matched = true;
    }
    try writer.writeAll("],\"tests\":[");
    count = 0;
    for (model.test_specs) |test_spec| {
        if (!testSpecMatchesImpact(model, &test_spec, target)) continue;
        if (count > 0) try writer.writeByte(',');
        try writer.print("{f}", .{std.json.fmt(test_spec, .{})});
        count += 1;
        matched = true;
    }
    try writer.writeAll("],\"implementations\":[");
    count = 0;
    for (model.implementations) |unit| {
        if (!implementationMatchesImpact(model, &unit, target)) continue;
        if (count > 0) try writer.writeByte(',');
        try writer.print("{f}", .{std.json.fmt(unit, .{})});
        count += 1;
        matched = true;
    }
    try writer.writeAll("]}\n");
    if (!matched) return error.ImpactTargetNotFound;
}

pub fn testSpecMatchesImpact(
    model: *const model_types.Graph,
    test_spec: *const model_types.TestSpec,
    target: []const u8,
) bool {
    if (std.mem.eql(u8, test_spec.id, target) or
        std.mem.eql(u8, test_spec.fixture.id, target) or
        std.mem.eql(u8, test_spec.fixture.path, target)) return true;
    for (model.test_bindings) |binding| {
        if (std.mem.eql(u8, binding.test_spec.id, test_spec.id) and
            std.mem.eql(u8, binding.source, target)) return true;
    }
    for (test_spec.claims) |claim| {
        if (std.mem.eql(u8, claim.oracle, target)) return true;
        for (claim.obligations) |reference| {
            if (std.mem.eql(u8, reference.id, target)) return true;
            const obligation = findObligation(model.verification, reference.id) orelse continue;
            if (std.mem.eql(u8, obligation.requirement.id, target)) return true;
        }
    }
    return false;
}

pub fn implementationMatchesImpact(
    model: *const model_types.Graph,
    unit: *const model_types.ImplementationUnit,
    target: []const u8,
) bool {
    if (std.mem.eql(u8, unit.id, target)) return true;
    for (unit.sources) |source| {
        if (std.mem.eql(u8, source.path, target)) return true;
    }
    for (unit.realizes) |reference| {
        if (std.mem.eql(u8, reference.id, target)) return true;
        const obligation = findObligation(model.verification, reference.id) orelse continue;
        if (std.mem.eql(u8, obligation.requirement.id, target)) return true;
    }
    return false;
}

test "missing queries fail explicitly" {
    const model: 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.RequirementNotFound,
        queryRequirement(&model, "REQ-MISSING-01", false, &output.writer),
    );
    try std.testing.expectError(
        error.ConceptNotFound,
        queryConcept(&model, "missing", false, &output.writer),
    );
    try std.testing.expectError(
        error.DocumentNotFound,
        queryDocument(&model, "missing", false, &output.writer),
    );
    try std.testing.expectError(
        error.TestSpecNotFound,
        queryTestSpec(&model, "missing", false, &output.writer),
    );
}