Skip to documentation
SLOP

tiny.smg.rules

Reference tiny.smg rules

Defined in tiny.smg.

API (35)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.smgrules
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsrules.CheckinvariantrulescheckFileDirectoryCollisionsprivate; no linktools.smg.src.rules.CheckparentFilterNodeFindingPlanactivateNodeFindingPlanappendNodeFindingPlandeinitNodeFindingPlaninit+4 morerules.CheckfileDirectoryCollisions
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsrules.CheckrulerulescheckInvariantrules.CheckfileDirectoryCollisionsprivate; no linktools.smg.src.rulescheckConceptsprivate; no linktools.smg.src.rulescheckCyclesprivate; no linktools.smg.src.rulescheckDeadprivate; no linktools.smg.src.rulescheckLayeringrulescheckNamespaceHandleImportsrules.Checkinvariant
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callscommand.checkrunrulescheckrulescheckInvariantrulescheckRuletest; no linktools.smg.src.rulestest: prepared check fills one parent...rules.Checkprepare
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallscommand.checkrunrulescheckrulescheckRuletest; no linktools.smg.src.rulestest: prepared check fills one parent...rules.CheckinvariantrulescheckDenyprivate; no linktools.smg.src.rulescheckQuantifiedrules.Checkrule
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsrulesaddStoredrulesadd
Static calls · unresolved targets: 2 · external targets: 0.
Called byCallsNo direct callersrulesaddrulesloadprivate; no linktools.smg.src.rulessavestorage.storeopenMutationrulesaddStored
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsrulescheckErrorMessagetest; no linktools.smg.src.rulestest: quantified assertion parse erro...private; no linktools.smg.src.rulesunsupportedCallMessagerulesassertionErrorMessage
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallstest; no linktools.smg.src.rulestest: prepared check fills one parent...rules.Checkpreparerules.Checkrulerulescheck
Static calls · unresolved targets: 2 · external targets: 0.
Called byCallsrules.Checkruletest; no linktools.smg.src.rulestest: deny namespace boundary rejects...test; no linktools.smg.src.rulestest: deny source exclusion preserves...test; no linktools.smg.src.rulestest: edge findings allocate only exa...EdgeFindingPlanactivateEdgeFindingPlanappendEdgeFindingPlandeinitEdgeFindingPlaninitEdgeFindingPlansort+4 morerulescheckDeny
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallscommand.checkruntest; no linktools.smg.src.rulestest: quantified assertions support p...rulesassertionErrorMessagerulescheckErrorMessage
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest; no linktools.smg.src.rulestest: file directory collisions admit...test; no linktools.smg.src.rulestest: file directory collisions alloc...test; no linktools.smg.src.rulestest: file directory collisions exemp...test; no linktools.smg.src.rulestest: file directory collisions ignor...rules.CheckfileDirectoryCollisionsrulescheckFileDirectoryCollisions
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest; no linktools.smg.src.rulestest: cycle invariant counts self cou...test; no linktools.smg.src.rulestest: cycle invariant reports the fir...test; no linktools.smg.src.rulestest: file directory collisions scope...rules.Checkinvariantrules.CheckpreparerulescheckInvariant
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsrules.Checkinvarianttest; no linktools.smg.src.rulestest: namespace handle import baselin...test; no linktools.smg.src.rulestest: namespace handle import baselin...test; no linktools.smg.src.rulestest: namespace handle import baselin...test; no linktools.smg.src.rulestest: namespace handle import baselin...+6 moreEdgeFindingPlanactivateEdgeFindingPlanappendEdgeFindingPlandeinitEdgeFindingPlaninitEdgeFindingPlansort+5 morerulescheckNamespaceHandleImports
Static calls · unresolved targets: 2 · external targets: 0.
Called byCallstest; no linktools.smg.src.rulestest: quantified assertions support p...test; no linktools.smg.src.rulestest: quantified checks preserve pyth...test; no linktools.smg.src.rulestest: quantified in cycle is subject ...test; no linktools.smg.src.rulestest: quantified rules use class and ...test; no linktools.smg.src.rulestest: quantified total metrics use an...rules.Checkpreparerules.CheckrulerulescheckRule
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallscommand.checkruntest; no linktools.smg.src.rulestest: deadRules distinguishes absence...private; no linktools.smg.src.rulesdeadRuleReasonprivate; no linktools.smg.src.rulesmarkPresentPatternsrulesdeadRules
Static calls · unresolved targets: 2 · external targets: 2.
Called byCallscommand.checkrunrulesaddStoredrulesremoveStoredrulessetNamespaceHandleImportBaselineStoredtest; no linktools.smg.src.rulestest: deny empty-target policy persis...private; no linktools.smg.src.rulesboolFieldprivate; no linktools.smg.src.rulesparamsFromObjectprivate; no linktools.smg.src.rulesstringFieldstorage.filesloadRulesrulesload
Static calls · unresolved targets: 3 · external targets: 1.
Called byCallsNo direct callsrulescheckDenyprivate; no linktools.smg.src.rulescollectDeadRulePatternsprivate; no linktools.smg.src.rulesdeadRuleReasontest; no linktools.smg.src.rulestest: deny parser and rule json use p...rulesparseDenyPattern
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallscommand.checkruntest; no linktools.smg.src.rulestest: quantified metric validation me...private; no linktools.smg.src.rulesglobprivate; no linktools.smg.src.rulesinScopeprivate; no linktools.smg.src.rulesmetricValidationMessageprivate; no linktools.smg.src.rulesparseAssertionprivate; no linktools.smg.src.rulessortedIdentifiersviewallNodesrulesquantifiedMetricValidationMessage
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsrulesremoveStoredrulesremove
Static calls · unresolved targets: 2 · external targets: 0.
Called byCallsNo direct callersrulesloadrulesremoveprivate; no linktools.smg.src.rulessavestorage.storeopenMutationrulesremoveStored
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsprivate; no linktools.smg.src.rulessaveprivate; no linktools.smg.src.rulessavePreservingOrdertest; no linktools.smg.src.rulestest: deny parser and rule json use p...private; no linktools.smg.src.rulesfieldrulesrender
Static calls · unresolved targets: 1 · external targets: 7.
Called byCallsrulessetNamespaceHandleImportBaselineStoredtest; no linktools.smg.src.rulestest: namespace import baseline updat...private; no linktools.smg.src.rulesparseNamespaceHandleImportBaselinerulessetNamespaceHandleImportBaseline
Static calls · unresolved targets: 1 · external targets: 2.
Called byCallsNo direct callersrulesloadprivate; no linktools.smg.src.rulessavePreservingOrderrulessetNamespaceHandleImportBaselinestorage.storeopenMutationrulessetNamespaceHandleImportBaselineStored
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsNo direct callersprivate; no linktools.smg.src.rulesknownMetricprivate; no linktools.smg.src.rulesparseAssertionrulesunknownAssertionMetrics
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallsNo direct callersprivate; no linktools.smg.src.rulesparseAssertionprivate; no linktools.smg.src.rulesvalidateMetricsrulesvalidateAssertion
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callersprivate; no linktools.smg.src.rulesparseNamespaceHandleImportBaselinerulesvalidateNamespaceHandleImportBaseline
Static calls · unresolved targets: 0 · external targets: 0.

Source: tools/smg/src/root.zig:29

zig
pub const rules = @import("rules.zig");

Source: tools/smg/src/rules.zig

zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const pretty = @import("pretty");const analysis = @import("analysis/root.zig");const exports = @import("export.zig");const graph_mod = @import("graph.zig");const limits_mod = @import("limits/root.zig");const view = @import("view.zig");const model = @import("model.zig");const storage = @import("storage/root.zig");const concepts_mod = @import("concepts.zig");const text = @import("text/root.zig");const pretty_json = pretty.json;const testing_analysis_limits = @import("root.zig").default_limits.analysis;pub const Rule = struct {    name: []const u8,    type: []const u8,    pattern: ?[]const u8 = null,    exclude_source: ?[]const u8 = null,    allow_empty_target: bool = false,    invariant: ?[]const u8 = null,    selector: ?[]const u8 = null,    assertion: ?[]const u8 = null,    params: []const model.Pair = &.{},    scope: ?[]const u8 = null,};pub const Finding = struct {    rule: []const u8,    type: []const u8,    message: []const u8,    edges: []const model.Edge = &.{},    nodes: []const []const u8 = &.{},    cycles: []const []const []const u8 = &.{},    predicates: []const PredicateWitness = &.{},};pub const EdgeFindingPlan = struct {    pub const Limits = struct {        edges: usize,    };    pub const Capacity = struct {        edges: usize,        bytes: usize,        pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {            return .{                .edges = limits.edges,                .bytes = std.math.mul(                    usize,                    limits.edges,                    @sizeOf(model.Edge),                ) catch return error.CapacityOverflow,            };        }    };    pub const InitError = std.mem.Allocator.Error || error{CapacityOverflow};    pub const claim: alloc_phase.capacity.Declaration = .{        .source = .{            .id = "smg.edge_finding_plan",            .kind = .phase_static,            .limit_source = .caller,            .storage = .{                .covered = &.{                    .{                        .id = "exact_edge_finding_result",                        .lifetime = .transferred,                        .detail = "exact edge finding result",                    },                },                .excluded = &.{                    "borrowed graph nodes edges indexes and model strings",                    "parsed rule fields and finding message text",                    "rule aggregation rendering and terminal output",                },            },            .capacity = .{                .inputs = &.{                    alloc_phase.capacity.bindInput(Limits, "edges", "edges"),                },                .type_selectors = &.{                    alloc_phase.capacity.bindType(model.Edge, "edge"),                },                .nodes = &.{                    .{ .input = 0 },                    .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },                },                .assertions = &.{.{                    .scope = .closure_total,                    .measure = .retained,                    .relation = .exact,                    .expression = 1,                }},            },            .overload = .{                .kind = .reject_before_seal,                .detail = "checked survey arithmetic and exact acquisition reject before fill",            },            .risks = .{                .transitive = .{                    .status = .witnessed,                    .detail = "sealed fill and in-place ordering call no allocator-backed child",                },                .foreign = .{                    .status = .excluded,                    .detail = "rule matching and result production perform no foreign effects",                },            },            .obligations = &.{                .{ .key = "smg_edge_finding_plan_capacity_capacity_model", .role = .capacity_model },                .{ .key = "smg_edge_finding_plan_capacity_overload", .role = .overload },                .{ .key = "smg_edge_finding_plan_oom", .role = .overload },                .{ .key = "smg_edge_finding_plan_sealed", .role = .transitive_risk },                .{ .key = "smg_edge_finding_plan_integration", .role = .foreign_risk },            },        },        .bindings = .{            .owner = @This(),            .seal = .{                .family = alloc_phase.capacity.selector(@This().activate),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },            .teardown = .{                .family = alloc_phase.capacity.selector(@This().deinit),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },        },    };    phase: alloc_phase.capacity.Phase,    capacity: Capacity,    edges: []model.Edge,    filled: usize = 0,    pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!EdgeFindingPlan {        const capacity = try Capacity.derive(limits);        return .{            .phase = .initialization,            .capacity = capacity,            .edges = try allocFindingEdges(allocator, capacity.edges),        };    }    pub fn activate(self: *EdgeFindingPlan) void {        std.debug.assert(self.phase == .initialization);        std.debug.assert(self.filled == 0);        self.phase = .steady;    }    pub fn append(self: *EdgeFindingPlan, edge: model.Edge) void {        std.debug.assert(self.phase == .steady);        std.debug.assert(self.filled < self.edges.len);        self.edges[self.filled] = edge;        self.filled += 1;    }    pub fn sort(self: *EdgeFindingPlan) void {        std.debug.assert(self.phase == .steady);        std.debug.assert(self.filled == self.edges.len);        view.sortEdges(self.edges);    }    pub fn transfer(self: *EdgeFindingPlan) []const model.Edge {        std.debug.assert(self.phase == .steady);        std.debug.assert(self.filled == self.edges.len);        const edges = self.edges;        self.phase = .teardown;        self.edges = @constCast((&[_]model.Edge{})[0..]);        self.filled = 0;        return edges;    }    pub fn deinit(self: *EdgeFindingPlan, allocator: std.mem.Allocator) void {        std.debug.assert(self.phase != .teardown);        self.phase = .teardown;        freeFindingEdges(allocator, self.edges);        self.* = undefined;    }};comptime {    alloc_phase.capacity.requireAllocatorExactOwnerShape(EdgeFindingPlan);}pub const NodeFindingPlan = struct {    pub const Limits = struct {        nodes: usize,    };    pub const Capacity = struct {        nodes: usize,        bytes: usize,        pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {            return .{                .nodes = limits.nodes,                .bytes = std.math.mul(                    usize,                    limits.nodes,                    @sizeOf([]const u8),                ) catch return error.CapacityOverflow,            };        }    };    pub const InitError = std.mem.Allocator.Error || error{CapacityOverflow};    pub const claim: alloc_phase.capacity.Declaration = .{        .source = .{            .id = "smg.node_finding_plan",            .kind = .phase_static,            .limit_source = .caller,            .storage = .{                .covered = &.{                    .{                        .id = "exact_node_finding_result",                        .lifetime = .transferred,                        .detail = "exact node finding result",                    },                },                .excluded = &.{                    "borrowed graph nodes indexes and model strings",                    "parsed rule fields and finding message text",                    "rule aggregation rendering and terminal output",                },            },            .capacity = .{                .inputs = &.{                    alloc_phase.capacity.bindInput(Limits, "nodes", "nodes"),                },                .type_selectors = &.{                    alloc_phase.capacity.bindType([]const u8, "const_u8"),                },                .nodes = &.{                    .{ .input = 0 },                    .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },                },                .assertions = &.{.{                    .scope = .closure_total,                    .measure = .retained,                    .relation = .exact,                    .expression = 1,                }},            },            .overload = .{                .kind = .reject_before_seal,                .detail = "checked survey arithmetic and exact acquisition reject before fill",            },            .risks = .{                .transitive = .{                    .status = .witnessed,                    .detail = "sealed fill and in-place ordering call no allocator-backed child",                },                .foreign = .{                    .status = .excluded,                    .detail = "rule matching and result production perform no foreign effects",                },            },            .obligations = &.{                .{ .key = "smg_node_finding_plan_capacity_capacity_model", .role = .capacity_model },                .{ .key = "smg_node_finding_plan_capacity_overload", .role = .overload },                .{ .key = "smg_node_finding_plan_oom", .role = .overload },                .{ .key = "smg_node_finding_plan_sealed", .role = .transitive_risk },                .{ .key = "smg_node_finding_plan_integration", .role = .foreign_risk },            },        },        .bindings = .{            .owner = @This(),            .seal = .{                .family = alloc_phase.capacity.selector(@This().activate),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },            .teardown = .{                .family = alloc_phase.capacity.selector(@This().deinit),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },        },    };    phase: alloc_phase.capacity.Phase,    capacity: Capacity,    nodes: [][]const u8,    filled: usize = 0,    pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!NodeFindingPlan {        const capacity = try Capacity.derive(limits);        return .{            .phase = .initialization,            .capacity = capacity,            .nodes = try allocFindingNodes(allocator, capacity.nodes),        };    }    pub fn activate(self: *NodeFindingPlan) void {        std.debug.assert(self.phase == .initialization);        std.debug.assert(self.filled == 0);        self.phase = .steady;    }    pub fn append(self: *NodeFindingPlan, node: []const u8) void {        std.debug.assert(self.phase == .steady);        std.debug.assert(self.filled < self.nodes.len);        self.nodes[self.filled] = node;        self.filled += 1;    }    pub fn sort(self: *NodeFindingPlan) void {        std.debug.assert(self.phase == .steady);        std.debug.assert(self.filled == self.nodes.len);        std.mem.sort([]const u8, self.nodes, {}, cmpString);    }    pub fn transfer(self: *NodeFindingPlan) []const []const u8 {        std.debug.assert(self.phase == .steady);        std.debug.assert(self.filled == self.nodes.len);        const nodes = self.nodes;        self.phase = .teardown;        self.nodes = @constCast((&[_][]const u8{})[0..]);        self.filled = 0;        return nodes;    }    pub fn deinit(self: *NodeFindingPlan, allocator: std.mem.Allocator) void {        std.debug.assert(self.phase != .teardown);        self.phase = .teardown;        freeFindingNodes(allocator, self.nodes);        self.* = undefined;    }};comptime {    alloc_phase.capacity.requireAllocatorExactOwnerShape(NodeFindingPlan);}pub const FactValue = union(enum) {    number: f64,    float_number: f64,    boolean: bool,};pub const PredicateFact = struct {    name: []const u8,    value: FactValue,};pub const PredicateWitness = struct {    subject: []const u8,    assertion: []const u8,    facts: []const PredicateFact,};pub fn load(    allocator: std.mem.Allocator,    root: []const u8,    limits: limits_mod.Storage,) ![]const Rule {    const values = try storage.files.loadRules(allocator, root, limits);    var out: std.ArrayList(Rule) = .empty;    for (values) |value| {        const object = switch (value) {            .object => |object| object,            else => continue,        };        const name = stringField(object, "name") orelse continue;        const type_name = stringField(object, "type") orelse continue;        try out.append(allocator, .{            .name = try allocator.dupe(u8, name),            .type = try allocator.dupe(u8, type_name),            .pattern = if (stringField(object, "pattern")) |v| try allocator.dupe(u8, v) else null,            .exclude_source = if (stringField(object, "exclude_source")) |raw|                try allocator.dupe(u8, raw)            else                null,            .allow_empty_target = boolField(object, "allow_empty_target") orelse false,            .invariant = if (stringField(object, "invariant")) |v| try allocator.dupe(u8, v) else null,            .selector = if (stringField(object, "selector")) |v| try allocator.dupe(u8, v) else null,            .assertion = if (stringField(object, "assertion")) |v| try allocator.dupe(u8, v) else null,            .params = try paramsFromObject(allocator, object.get("params")),            .scope = if (stringField(object, "scope")) |v| try allocator.dupe(u8, v) else null,        });    }    return try out.toOwnedSlice(allocator);}fn save(allocator: std.mem.Allocator, root: []const u8, rule_list: []const Rule) !void {    var sorted = try allocator.alloc(Rule, rule_list.len);    for (rule_list, 0..) |rule, index| sorted[index] = rule;    std.mem.sort(Rule, sorted, {}, cmpRule);    var lines: std.ArrayList([]const u8) = .empty;    for (sorted) |rule| try lines.append(allocator, try render(allocator, rule));    try storage.files.saveRules(allocator, root, lines.items);}fn savePreservingOrder(allocator: std.mem.Allocator, root: []const u8, rule_list: []const Rule) !void {    var lines: std.ArrayList([]const u8) = .empty;    for (rule_list) |rule| try lines.append(allocator, try render(allocator, rule));    try storage.files.saveRules(allocator, root, lines.items);}pub fn addStored(    allocator: std.mem.Allocator,    root: []const u8,    rule: Rule,    limits: limits_mod.Storage,) !void {    var opened = try storage.store.openMutation(allocator, root, limits);    defer opened.close();    const rule_list = try load(allocator, root, limits);    try save(allocator, root, try add(allocator, rule_list, rule));}pub fn removeStored(    allocator: std.mem.Allocator,    root: []const u8,    name: []const u8,    limits: limits_mod.Storage,) !void {    var opened = try storage.store.openMutation(allocator, root, limits);    defer opened.close();    const rule_list = try load(allocator, root, limits);    try save(allocator, root, try remove(allocator, rule_list, name));}pub fn setNamespaceHandleImportBaselineStored(    allocator: std.mem.Allocator,    root: []const u8,    name: []const u8,    baseline: []const u8,    limits: limits_mod.Storage,) !void {    var opened = try storage.store.openMutation(allocator, root, limits);    defer opened.close();    const rule_list = try load(allocator, root, limits);    try savePreservingOrder(        allocator,        root,        try setNamespaceHandleImportBaseline(allocator, rule_list, name, baseline),    );}pub fn add(allocator: std.mem.Allocator, rule_list: []const Rule, rule: Rule) ![]const Rule {    for (rule_list) |existing| if (std.mem.eql(u8, existing.name, rule.name)) return error.DuplicateRule;    var out: std.ArrayList(Rule) = .empty;    for (rule_list) |existing| try out.append(allocator, existing);    try out.append(allocator, rule);    return try out.toOwnedSlice(allocator);}pub fn remove(allocator: std.mem.Allocator, rule_list: []const Rule, name: []const u8) ![]const Rule {    var out: std.ArrayList(Rule) = .empty;    var found = false;    for (rule_list) |rule| {        if (std.mem.eql(u8, rule.name, name)) {            found = true;        } else {            try out.append(allocator, rule);        }    }    if (!found) return error.RuleNotFound;    return try out.toOwnedSlice(allocator);}pub fn setNamespaceHandleImportBaseline(    allocator: std.mem.Allocator,    rule_list: []const Rule,    name: []const u8,    baseline: []const u8,) ![]const Rule {    _ = try parseNamespaceHandleImportBaseline(baseline);    const out = try allocator.dupe(Rule, rule_list);    var found = false;    for (out) |*rule| {        if (!std.mem.eql(u8, rule.name, name)) continue;        found = true;        if (!std.mem.eql(u8, rule.type, "invariant") or            !std.mem.eql(u8, rule.invariant orelse "", "namespace-handle-imports"))        {            return error.InvalidRule;        }        var params: std.ArrayList(model.Pair) = .empty;        var replaced = false;        for (rule.params) |param| {            if (std.mem.eql(u8, param.key, "baseline")) {                try params.append(allocator, .{                    .key = try allocator.dupe(u8, "baseline"),                    .value = try allocator.dupe(u8, baseline),                });                replaced = true;            } else {                try params.append(allocator, param);            }        }        if (!replaced) {            try params.append(allocator, .{                .key = try allocator.dupe(u8, "baseline"),                .value = try allocator.dupe(u8, baseline),            });        }        rule.params = try params.toOwnedSlice(allocator);    }    if (!found) return error.RuleNotFound;    return out;}pub const Check = struct {    graph: graph_mod.Graph,    analysis_limits: limits_mod.Analysis,    parents: ParentFilter,    parents_ready: bool,    pub fn prepare(self: *Check, graph: graph_mod.Graph, limits: limits_mod.Analysis) void {        self.graph = graph;        self.analysis_limits = limits;        self.parents_ready = false;    }    pub fn rule(        self: *Check,        allocator: std.mem.Allocator,        value: Rule,        concept_list: []const concepts_mod.Concept,    ) !?Finding {        const graph = self.graph;        if (std.mem.eql(u8, value.type, "deny")) return try checkDeny(allocator, graph, value);        if (std.mem.eql(u8, value.type, "invariant")) return try self.invariant(allocator, value, concept_list);        if (std.mem.eql(u8, value.type, "quantified")) return try checkQuantified(allocator, graph, value, self.analysis_limits);        return error.InvalidRule;    }    pub fn invariant(        self: *Check,        allocator: std.mem.Allocator,        value: Rule,        concept_list: []const concepts_mod.Concept,    ) !?Finding {        const graph = self.graph;        const name = value.invariant orelse return error.InvalidRule;        if (std.mem.eql(u8, name, "no-cycles")) return try checkCycles(allocator, graph, value);        if (std.mem.eql(u8, name, "no-dead-code")) return try checkDead(allocator, graph, value);        if (std.mem.eql(u8, name, "no-layering-violations")) return try checkLayering(allocator, graph, value);        if (std.mem.eql(u8, name, "concept-boundaries")) return try checkConcepts(allocator, graph, value, concept_list);        if (std.mem.eql(u8, name, "namespace-handle-imports")) return try checkNamespaceHandleImports(allocator, graph, value);        if (std.mem.eql(u8, name, "file-directory-collisions")) return try self.fileDirectoryCollisions(allocator, value);        return error.UnknownInvariant;    }    pub fn fileDirectoryCollisions(        self: *Check,        allocator: std.mem.Allocator,        value: Rule,    ) !?Finding {        const parents = self.parentFilter();        const count = try fileDirectoryCollisionCount(self.graph, value, parents);        if (count == 0) return null;        var plan = try NodeFindingPlan.init(allocator, .{ .nodes = count });        errdefer plan.deinit(allocator);        plan.activate();        for (self.graph.nodes.items) |node| {            if (fileDirectoryCollision(self.graph, value, node, parents)) plan.append(node.name);        }        plan.sort();        const message = try std.fmt.allocPrint(allocator, "{d} file-directory concept collision(s)", .{count});        const nodes = plan.transfer();        return .{            .rule = value.name,            .type = "invariant",            .message = message,            .nodes = nodes,        };    }    fn parentFilter(self: *Check) *const ParentFilter {        if (!self.parents_ready) {            self.parents.fill(self.graph.nodes.items);            self.parents_ready = true;        }        std.debug.assert(self.parents_ready);        return &self.parents;    }};pub fn check(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule_list: []const Rule, maybe_name: ?[]const u8, concept_list: []const concepts_mod.Concept, limits: limits_mod.Analysis) ![]const Finding {    var findings: std.ArrayList(Finding) = .empty;    var prepared: Check = undefined;    prepared.prepare(graph, limits);    for (rule_list) |rule| {        if (maybe_name != null and !std.mem.eql(u8, rule.name, maybe_name.?)) continue;        if (try prepared.rule(allocator, rule, concept_list)) |finding| try findings.append(allocator, finding);    }    return try findings.toOwnedSlice(allocator);}pub fn checkRule(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule, concept_list: []const concepts_mod.Concept, limits: limits_mod.Analysis) !?Finding {    var prepared: Check = undefined;    prepared.prepare(graph, limits);    return try prepared.rule(allocator, rule, concept_list);}pub const DeadRule = struct {    rule: []const u8,    reason: []const u8,};const PatternPresence = struct {    pattern: []const u8,    found: bool = false,};pub fn deadRules(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule_list: []const Rule) ![]const DeadRule {    const pattern_capacity = std.math.mul(usize, rule_list.len, 2) catch return error.CapacityOverflow;    const pattern_storage = try allocator.alloc(PatternPresence, pattern_capacity);    defer allocator.free(pattern_storage);    const patterns = pattern_storage[0..collectDeadRulePatterns(pattern_storage, rule_list)];    markPresentPatterns(patterns, graph.nodes.items);    var out: std.ArrayList(DeadRule) = .empty;    for (rule_list) |rule| {        if (try deadRuleReason(allocator, patterns, rule)) |reason| {            try out.append(allocator, .{ .rule = rule.name, .reason = reason });        }    }    return try out.toOwnedSlice(allocator);}fn collectDeadRulePatterns(buffer: []PatternPresence, rule_list: []const Rule) usize {    var count: usize = 0;    for (rule_list) |rule| {        if (std.mem.eql(u8, rule.type, "deny")) {            const pattern = rule.pattern orelse continue;            const parsed = parseDenyPattern(pattern) catch continue;            appendUniquePattern(buffer, &count, parsed.source);            appendUniquePattern(buffer, &count, parsed.target);        } else if (std.mem.eql(u8, rule.type, "quantified")) {            const selector = rule.selector orelse continue;            appendUniquePattern(buffer, &count, selector);        }    }    return count;}fn appendUniquePattern(buffer: []PatternPresence, count: *usize, pattern: []const u8) void {    for (buffer[0..count.*]) |presence| {        if (std.mem.eql(u8, presence.pattern, pattern)) return;    }    std.debug.assert(count.* < buffer.len);    buffer[count.*] = .{ .pattern = pattern };    count.* += 1;}fn markPresentPatterns(patterns: []PatternPresence, nodes: []const model.Node) void {    var missing = patterns.len;    for (nodes) |node| {        for (patterns) |*presence| {            if (presence.found or !glob(node.name, presence.pattern)) continue;            presence.found = true;            missing -= 1;        }        if (missing == 0) return;    }}fn patternIsPresent(patterns: []const PatternPresence, pattern: []const u8) bool {    for (patterns) |presence| {        if (std.mem.eql(u8, presence.pattern, pattern)) return presence.found;    }    unreachable;}fn deadRuleReason(allocator: std.mem.Allocator, patterns: []const PatternPresence, rule: Rule) !?[]const u8 {    if (std.mem.eql(u8, rule.type, "deny")) {        const pattern = rule.pattern orelse return null;        const parsed = parseDenyPattern(pattern) catch return null;        if (!patternIsPresent(patterns, parsed.source)) {            return try std.fmt.allocPrint(allocator, "no nodes match source '{s}'", .{parsed.source});        }        if (!rule.allow_empty_target and !patternIsPresent(patterns, parsed.target)) {            return try std.fmt.allocPrint(allocator, "no nodes match target '{s}'", .{parsed.target});        }        return null;    }    if (std.mem.eql(u8, rule.type, "quantified")) {        const selector = rule.selector orelse return null;        if (!patternIsPresent(patterns, selector)) {            return try std.fmt.allocPrint(allocator, "no nodes match selector '{s}'", .{selector});        }        return null;    }    return null;}pub fn checkDeny(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule) !?Finding {    const pattern = rule.pattern orelse return error.InvalidRule;    const parsed = try parseDenyPattern(pattern);    const count = try denyEdgeCount(graph, rule, parsed);    if (count == 0) return null;    var plan = try EdgeFindingPlan.init(allocator, .{ .edges = count });    errdefer plan.deinit(allocator);    plan.activate();    for (graph.edges.items) |edge| {        if (denyEdgeMatches(rule, parsed, edge)) plan.append(edge);    }    plan.sort();    const message = try std.fmt.allocPrint(allocator, "{d} forbidden edge(s)", .{count});    const edges = plan.transfer();    return .{        .rule = rule.name,        .type = "deny",        .message = message,        .edges = edges,    };}fn denyEdgeCount(graph: graph_mod.Graph, rule: Rule, parsed: Deny) error{CapacityOverflow}!usize {    var count: usize = 0;    for (graph.edges.items) |edge| {        if (!denyEdgeMatches(rule, parsed, edge)) continue;        count = std.math.add(usize, count, 1) catch return error.CapacityOverflow;    }    return count;}fn denyEdgeMatches(rule: Rule, parsed: Deny, edge: model.Edge) bool {    if (parsed.rel) |rel| {        if (!std.mem.eql(u8, edge.rel, rel)) return false;    } else if (!exports.isCoupling(edge.rel)) {        return false;    }    if (rule.scope) |scope| {        if (!inScope(edge.source, scope)) return false;    }    if (!glob(edge.source, parsed.source) or !glob(edge.target, parsed.target)) return false;    if (rule.exclude_source) |pattern| {        if (glob(edge.source, pattern)) return false;    }    return true;}fn allocFindingEdges(allocator: std.mem.Allocator, count: usize) std.mem.Allocator.Error![]model.Edge {    if (count == 0) return @constCast((&[_]model.Edge{})[0..]);    return try allocator.alloc(model.Edge, count);}fn freeFindingEdges(allocator: std.mem.Allocator, edges: []model.Edge) void {    if (edges.len != 0) allocator.free(edges);}fn allocFindingNodes(allocator: std.mem.Allocator, count: usize) std.mem.Allocator.Error![][]const u8 {    if (count == 0) return @constCast((&[_][]const u8{})[0..]);    return try allocator.alloc([]const u8, count);}fn freeFindingNodes(allocator: std.mem.Allocator, nodes: [][]const u8) void {    if (nodes.len != 0) allocator.free(nodes);}pub fn checkInvariant(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule, concept_list: []const concepts_mod.Concept, limits: limits_mod.Analysis) !?Finding {    var prepared: Check = undefined;    prepared.prepare(graph, limits);    return try prepared.invariant(allocator, rule, concept_list);}const root_module_suffix = ".root";const root_file_name = "root.zig";const build_file_name = "build.zig";const test_file_name = "test.zig";const zig_file_suffix = ".zig";const owner_name_capacity = std.fs.max_path_bytes + root_module_suffix.len;const parent_filter_bits: usize = 1 << 18;const parent_filter_words: usize = parent_filter_bits / 64;const ParentFilter = struct {    words: [parent_filter_words]u64,    holds_files: bool,    fn fill(self: *ParentFilter, nodes: []const model.Node) void {        @memset(&self.words, 0);        self.holds_files = false;        var previous: []const u8 = &.{};        for (nodes) |node| {            const file = node.file orelse continue;            if (file.len == 0) continue;            self.holds_files = true;            if (std.mem.eql(u8, file, previous)) continue;            previous = file;            var end: usize = 1;            while (end < file.len) : (end += 1) {                if (!pathSeparatorBoundary(file, end)) continue;                self.mark(file[0..end]);            }        }    }    fn mark(self: *ParentFilter, directory: []const u8) void {        for (parentFilterSlots(directory)) |slot| {            const shift: u6 = @truncate(slot);            self.words[slot / 64] |= @as(u64, 1) << shift;        }    }    fn admits(self: *const ParentFilter, directory: []const u8) bool {        if (directory.len == 0) return self.holds_files;        for (parentFilterSlots(directory)) |slot| {            const shift: u6 = @truncate(slot);            if (self.words[slot / 64] & (@as(u64, 1) << shift) == 0) return false;        }        return true;    }};fn pathSeparatorBoundary(path: []const u8, end: usize) bool {    std.debug.assert(end != 0);    std.debug.assert(end < path.len);    return std.fs.path.isSep(path[end]) or std.fs.path.isSep(path[end - 1]);}fn parentFilterSlots(directory: []const u8) [2]usize {    std.debug.assert(directory.len != 0);    const digest = std.hash_map.hashString(directory);    const mask: u64 = parent_filter_bits - 1;    const slots: [2]usize = .{ @intCast(digest & mask), @intCast((digest >> 32) & mask) };    std.debug.assert(slots[0] < parent_filter_bits);    std.debug.assert(slots[1] < parent_filter_bits);    return slots;}pub fn checkFileDirectoryCollisions(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule) !?Finding {    var prepared: Check = undefined;    prepared.graph = graph;    prepared.parents_ready = false;    return try prepared.fileDirectoryCollisions(allocator, rule);}fn fileDirectoryCollisionCount(    graph: graph_mod.Graph,    rule: Rule,    parents: *const ParentFilter,) error{CapacityOverflow}!usize {    var count: usize = 0;    for (graph.nodes.items) |node| {        if (!fileDirectoryCollision(graph, rule, node, parents)) continue;        count = std.math.add(usize, count, 1) catch return error.CapacityOverflow;    }    return count;}fn fileDirectoryCollision(    graph: graph_mod.Graph,    rule: Rule,    source: model.Node,    parents: *const ParentFilter,) bool {    if (!isFileBackedZigModule(source)) return false;    if (rule.scope) |scope| {        if (!inScope(source.name, scope)) return false;    }    const source_file = source.file.?;    if (fileDirectoryAggregate(source_file)) return false;    if (source_file.len <= zig_file_suffix.len) return false;    const directory = source_file[0 .. source_file.len - zig_file_suffix.len];    if (!parents.admits(directory)) return false;    for (graph.nodes.items) |target| {        const target_file = target.file orelse continue;        if (pathIsStrictlyUnder(target_file, directory)) return true;    }    return false;}fn fileDirectoryAggregate(file: []const u8) bool {    const basename = std.fs.path.basename(file);    return std.mem.eql(u8, basename, build_file_name) or        std.mem.eql(u8, basename, test_file_name);}pub fn checkNamespaceHandleImports(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule) !?Finding {    var owner_name_buffer: [owner_name_capacity]u8 = undefined;    const count = try namespaceHandleImportCount(graph, rule, &owner_name_buffer);    const baseline = try namespaceHandleImportBaseline(rule);    if (count == 0 and baseline == null) return null;    var plan = try EdgeFindingPlan.init(allocator, .{ .edges = count });    errdefer plan.deinit(allocator);    plan.activate();    for (graph.edges.items) |edge| {        if (try namespaceHandleImportViolation(graph, rule, edge, &owner_name_buffer)) plan.append(edge);    }    plan.sort();    const actual_digest = namespaceHandleImportDigest(plan.edges);    if (baseline) |expected| {        if (expected.count == count and std.mem.eql(u8, &expected.digest, &actual_digest)) {            plan.deinit(allocator);            return null;        }    }    const message = if (baseline) |expected| baseline_message: {        const change = if (count < expected.count)            "count decreased"        else if (count > expected.count)            "count increased"        else            "edge set replaced at unchanged count";        const actual_text = std.fmt.bytesToHex(actual_digest, .lower);        break :baseline_message try std.fmt.allocPrint(            allocator,            "namespace implementation import baseline changed ({s}): expected {d}:{s}, actual {d}:{s}; review witnesses, then accept with `smg rule baseline {s} {d}:{s}`",            .{                change,                expected.count,                std.fmt.bytesToHex(expected.digest, .lower),                count,                actual_text,                rule.name,                count,                actual_text,            },        );    } else try std.fmt.allocPrint(allocator, "{d} namespace implementation import(s)", .{count});    const edges = plan.transfer();    return .{        .rule = rule.name,        .type = "invariant",        .message = message,        .edges = edges,    };}const NamespaceHandleImportBaseline = struct {    count: usize,    digest: [std.crypto.hash.sha2.Sha256.digest_length]u8,};fn namespaceHandleImportBaseline(rule: Rule) !?NamespaceHandleImportBaseline {    const raw = model.pairValue(rule.params, "baseline") orelse return null;    return try parseNamespaceHandleImportBaseline(raw);}fn parseNamespaceHandleImportBaseline(raw: []const u8) !NamespaceHandleImportBaseline {    const separator = std.mem.indexOfScalar(u8, raw, ':') orelse return error.InvalidRule;    if (separator == 0 or separator + 1 >= raw.len) return error.InvalidRule;    const count = std.fmt.parseInt(usize, raw[0..separator], 10) catch return error.InvalidRule;    const digest_text = raw[separator + 1 ..];    if (digest_text.len != std.crypto.hash.sha2.Sha256.digest_length * 2) return error.InvalidRule;    var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;    _ = std.fmt.hexToBytes(&digest, digest_text) catch return error.InvalidRule;    return .{ .count = count, .digest = digest };}pub fn validateNamespaceHandleImportBaseline(raw: []const u8) !void {    _ = try parseNamespaceHandleImportBaseline(raw);}fn namespaceHandleImportDigest(edges: []const model.Edge) [std.crypto.hash.sha2.Sha256.digest_length]u8 {    var hasher = std.crypto.hash.sha2.Sha256.init(.{});    for (edges) |edge| {        hasher.update(edge.source);        hasher.update("\x00");        hasher.update(edge.target);        hasher.update("\n");    }    var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;    hasher.final(&digest);    return digest;}fn namespaceHandleImportCount(graph: graph_mod.Graph, rule: Rule, owner_name_buffer: []u8) !usize {    var count: usize = 0;    for (graph.edges.items) |edge| {        if (!try namespaceHandleImportViolation(graph, rule, edge, owner_name_buffer)) continue;        count = std.math.add(usize, count, 1) catch return error.CapacityOverflow;    }    return count;}fn namespaceHandleImportViolation(graph: graph_mod.Graph, rule: Rule, edge: model.Edge, owner_name_buffer: []u8) !bool {    if (!std.mem.eql(u8, edge.rel, model.RelType.imports)) return false;    if (rule.scope) |scope| {        if (!inScope(edge.source, scope)) return false;    }    const source = graph_mod.getNode(&graph, edge.source) orelse return false;    const target = graph_mod.getNode(&graph, edge.target) orelse return false;    if (!isFileBackedZigModule(source) or !isFileBackedZigModule(target)) return false;    const source_owner = try nearestRootOwner(graph, source, owner_name_buffer) orelse return false;    const target_owner = try nearestRootOwner(graph, target, owner_name_buffer) orelse return false;    if (sameRootOwner(source_owner, target_owner)) return false;    if (sameRootOwner(target, target_owner)) return false;    if (isChildTestDiscovery(source, source_owner, target, target_owner)) return false;    return true;}fn nearestRootOwner(graph: graph_mod.Graph, node: model.Node, owner_name_buffer: []u8) !?model.Node {    if (!isFileBackedZigModule(node)) return null;    if (isRootModule(node)) return node;    var prefix_end = std.mem.lastIndexOfScalar(u8, node.name, '.') orelse 0;    while (true) {        const candidate_name = if (prefix_end == 0)            "root"        else candidate: {            const name_len = std.math.add(usize, prefix_end, root_module_suffix.len) catch return error.CapacityOverflow;            if (name_len > owner_name_buffer.len) return error.CapacityOverflow;            @memcpy(owner_name_buffer[0..prefix_end], node.name[0..prefix_end]);            @memcpy(owner_name_buffer[prefix_end..name_len], root_module_suffix);            break :candidate owner_name_buffer[0..name_len];        };        if (graph_mod.getNode(&graph, candidate_name)) |candidate| {            if (isRootModule(candidate) and rootOwnsFile(candidate.file.?, node.file.?)) return candidate;        }        if (prefix_end == 0) return null;        prefix_end = std.mem.lastIndexOfScalar(u8, node.name[0..prefix_end], '.') orelse 0;    }}fn isFileBackedZigModule(node: model.Node) bool {    if (!std.mem.eql(u8, node.type, model.NodeType.module)) return false;    const file = node.file orelse return false;    return std.mem.endsWith(u8, file, zig_file_suffix);}fn isRootModule(node: model.Node) bool {    if (!isFileBackedZigModule(node)) return false;    return std.mem.eql(u8, std.fs.path.basename(node.file.?), root_file_name);}fn sameRootOwner(a: model.Node, b: model.Node) bool {    return std.mem.eql(u8, a.name, b.name) and std.mem.eql(u8, a.file.?, b.file.?);}fn rootOwnsFile(root_file: []const u8, file: []const u8) bool {    const root_directory = std.fs.path.dirname(root_file) orelse "";    return pathIsStrictlyUnder(file, root_directory);}fn isChildTestDiscovery(source: model.Node, source_owner: model.Node, target: model.Node, target_owner: model.Node) bool {    if (!std.mem.eql(u8, std.fs.path.basename(source.file.?), test_file_name)) return false;    if (!std.mem.eql(u8, std.fs.path.basename(target.file.?), test_file_name)) return false;    const source_directory = std.fs.path.dirname(source_owner.file.?) orelse "";    const target_directory = std.fs.path.dirname(target_owner.file.?) orelse "";    return pathIsStrictlyUnder(target_directory, source_directory);}fn pathIsStrictlyUnder(path: []const u8, directory: []const u8) bool {    if (directory.len == 0) return path.len != 0;    if (!std.mem.startsWith(u8, path, directory) or path.len == directory.len) return false;    if (std.fs.path.isSep(directory[directory.len - 1])) return true;    return std.fs.path.isSep(path[directory.len]);}fn checkCycles(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule) !?Finding {    const edges = try view.allEdges(graph, allocator);    for (edges) |edge| {        if (!exports.isCoupling(edge.rel)) continue;        if (!try couplingBackEdge(graph, edge)) continue;        const cycle = try allocator.alloc([]const u8, 2);        cycle[0] = edge.source;        cycle[1] = edge.target;        const cycles = try allocator.alloc([]const []const u8, 1);        cycles[0] = cycle;        return .{            .rule = rule.name,            .type = "invariant",            .message = "1 cycle(s)",            .cycles = cycles,        };    }    return null;}fn couplingBackEdge(graph: graph_mod.Graph, edge: model.Edge) !bool {    for (exports.coupling_relations) |relation| {        if (try view.containsEdge(graph, edge.target, relation, edge.source)) return true;    }    return false;}fn checkDead(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule) !?Finding {    var roots = std.StringHashMap(void).init(allocator);    const entry = model.pairValue(rule.params, "entry_points") orelse "";    if (entry.len != 0) {        var parts = std.mem.splitScalar(u8, entry, ',');        while (parts.next()) |raw| {            const pattern = text.trim(raw);            if (pattern.len == 0) continue;            for (graph.nodes.items) |node| if (glob(node.name, pattern)) try roots.put(node.name, {});        }    }    if (roots.count() == 0) {        for (graph.nodes.items) |node| {            const incoming = try view.incoming(graph, allocator, node.name, null);            if (incoming.len == 0) try roots.put(node.name, {});        }    }    var reachable = std.StringHashMap(void).init(allocator);    var queue: std.ArrayList([]const u8) = .empty;    var it = roots.iterator();    while (it.next()) |entry_item| {        try reachable.put(entry_item.key_ptr.*, {});        try queue.append(allocator, entry_item.key_ptr.*);    }    var head: usize = 0;    while (head < queue.items.len) : (head += 1) {        for (try view.outgoing(graph, allocator, queue.items[head], null)) |edge| {            if (!reachable.contains(edge.target)) {                try reachable.put(edge.target, {});                try queue.append(allocator, edge.target);            }        }    }    var dead: std.ArrayList([]const u8) = .empty;    for (graph.nodes.items) |node| {        if (!reachable.contains(node.name)) try dead.append(allocator, node.name);    }    std.mem.sort([]const u8, dead.items, {}, cmpString);    if (dead.items.len == 0) return null;    return .{ .rule = rule.name, .type = "invariant", .message = try std.fmt.allocPrint(allocator, "{d} unreferenced node(s)", .{dead.items.len}), .nodes = try dead.toOwnedSlice(allocator) };}fn checkLayering(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule) !?Finding {    const cycle = try checkCycles(allocator, graph, rule);    if (cycle == null) return null;    var edges: std.ArrayList(model.Edge) = .empty;    const all = try view.allEdges(graph, allocator);    for (all) |edge| if (exports.isCoupling(edge.rel)) try edges.append(allocator, edge);    return .{ .rule = rule.name, .type = "invariant", .message = try std.fmt.allocPrint(allocator, "{d} back-dependency edge(s)", .{edges.items.len}), .edges = try edges.toOwnedSlice(allocator) };}fn checkConcepts(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule, concept_list: []const concepts_mod.Concept) !?Finding {    if (concept_list.len == 0) return error.MissingConcepts;    const edges = try view.allEdges(graph, allocator);    var offending: std.ArrayList(model.Edge) = .empty;    for (edges) |edge| {        if (!exports.isCoupling(edge.rel)) continue;        const source = conceptName(concept_list, edge.source) orelse continue;        const target = conceptName(concept_list, edge.target) orelse continue;        if (!std.mem.eql(u8, source, target)) try offending.append(allocator, edge);    }    if (offending.items.len == 0) return null;    const first = offending.items[0];    const source = conceptName(concept_list, first.source).?;    const target = conceptName(concept_list, first.target).?;    return .{        .rule = rule.name,        .type = "invariant",        .message = try std.fmt.allocPrint(allocator, "{s}->{s}: {d} unsanctioned cross-concept edge(s)", .{ source, target, offending.items.len }),        .edges = try offending.toOwnedSlice(allocator),    };}fn checkQuantified(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule: Rule, limits: limits_mod.Analysis) !?Finding {    const selector = rule.selector orelse return error.InvalidRule;    const assertion = rule.assertion orelse return error.InvalidRule;    const parsed = try parseAssertion(allocator, assertion);    try validateMetrics(parsed.identifiers, rule.name);    var failing: std.ArrayList([]const u8) = .empty;    var predicates: std.ArrayList(PredicateWitness) = .empty;    for (try view.allNodes(graph, allocator, null)) |node| {        if (!glob(node.name, selector)) continue;        if (rule.scope) |scope| if (!inScope(node.name, scope)) continue;        const ok = try evaluateAssertion(allocator, graph, node, parsed, limits);        if (!ok) {            try failing.append(allocator, node.name);            try predicates.append(allocator, .{                .subject = node.name,                .assertion = assertion,                .facts = try predicateFacts(allocator, graph, node, parsed.identifiers, limits),            });        }    }    if (failing.items.len == 0) return null;    return .{        .rule = rule.name,        .type = "quantified",        .message = try std.fmt.allocPrint(allocator, "{d} subject(s) failed {s}", .{ failing.items.len, assertion }),        .nodes = try failing.toOwnedSlice(allocator),        .predicates = try predicates.toOwnedSlice(allocator),    };}fn predicateFacts(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, identifiers: []const []const u8, limits: limits_mod.Analysis) ![]const PredicateFact {    var facts: std.ArrayList(PredicateFact) = .empty;    for (try sortedIdentifiers(allocator, identifiers)) |identifier| {        if (factExists(facts.items, identifier)) continue;        try facts.append(allocator, .{            .name = identifier,            .value = try factValue(allocator, graph, node, identifier, limits),        });    }    return try facts.toOwnedSlice(allocator);}fn factExists(facts: []const PredicateFact, name: []const u8) bool {    for (facts) |fact| if (std.mem.eql(u8, fact.name, name)) return true;    return false;}fn factValue(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, metric: []const u8, limits: limits_mod.Analysis) !FactValue {    if (std.mem.eql(u8, metric, "dead") or std.mem.eql(u8, metric, "in_cycle")) return .{ .boolean = try metricBool(allocator, graph, node, metric, limits) };    if (isFloatMetric(metric)) return .{ .float_number = try metricValue(allocator, graph, node, metric, limits) };    return .{ .number = try metricValue(allocator, graph, node, metric, limits) };}pub fn validateAssertion(allocator: std.mem.Allocator, rule_name: []const u8, assertion: []const u8) !void {    const parsed = try parseAssertion(allocator, assertion);    try validateMetrics(parsed.identifiers, rule_name);}pub fn render(allocator: std.mem.Allocator, rule: Rule) ![]const u8 {    var out: std.Io.Writer.Allocating = .init(allocator);    errdefer out.deinit();    var writer = pretty_json.Writer.init(&out.writer, .minified);    try writer.beginObject();    try writer.objectField("kind");    try writer.write("rule");    try writer.objectField("name");    try writer.write(rule.name);    try writer.objectField("type");    try writer.write(rule.type);    if (rule.pattern) |value| try field(&writer, "pattern", value);    if (rule.exclude_source) |value| try field(&writer, "exclude_source", value);    if (rule.allow_empty_target) {        try writer.objectField("allow_empty_target");        try writer.write(true);    }    if (rule.invariant) |value| try field(&writer, "invariant", value);    if (rule.selector) |value| try field(&writer, "selector", value);    if (rule.assertion) |value| try field(&writer, "assertion", value);    if (rule.params.len != 0) {        try writer.objectField("params");        try writer.beginObject();        for (rule.params) |param| {            try writer.objectField(param.key);            if (param.json) {                try writer.raw(param.value);            } else {                try writer.write(param.value);            }        }        try writer.endObject();    }    if (rule.scope) |value| try field(&writer, "scope", value);    try writer.endObject();    return try out.toOwnedSlice();}fn field(writer: *pretty_json.Writer, name: []const u8, value: []const u8) !void {    try writer.objectField(name);    try writer.write(value);}const Deny = struct {    source: []const u8,    rel: ?[]const u8,    target: []const u8,};pub fn parseDenyPattern(value: []const u8) !Deny {    const trimmed = text.trim(value);    if (std.mem.indexOf(u8, trimmed, "-[")) |open| {        const close = std.mem.indexOf(u8, trimmed, "]->") orelse return error.InvalidDenyPattern;        return .{ .source = text.trim(trimmed[0..open]), .rel = text.trim(trimmed[open + 2 .. close]), .target = text.trim(trimmed[close + 3 ..]) };    }    const arrow = std.mem.indexOf(u8, trimmed, "->") orelse return error.InvalidDenyPattern;    return .{ .source = text.trim(trimmed[0..arrow]), .rel = null, .target = text.trim(trimmed[arrow + 2 ..]) };}pub fn unknownAssertionMetrics(allocator: std.mem.Allocator, assertion: []const u8) ![]const []const u8 {    const parsed = try parseAssertion(allocator, assertion);    var out: std.ArrayList([]const u8) = .empty;    for (parsed.identifiers) |identifier| if (!knownMetric(identifier)) try out.append(allocator, identifier);    return try out.toOwnedSlice(allocator);}pub fn quantifiedMetricValidationMessage(allocator: std.mem.Allocator, graph: graph_mod.Graph, rule_list: []const Rule) !?[]const u8 {    for (rule_list) |rule| {        if (!std.mem.eql(u8, rule.type, "quantified")) continue;        const selector = rule.selector orelse return try std.fmt.allocPrint(allocator, "quantified rule '{s}' has no selector", .{rule.name});        const assertion = rule.assertion orelse return try std.fmt.allocPrint(allocator, "quantified rule '{s}' has no assertion", .{rule.name});        const parsed = try parseAssertion(allocator, assertion);        const identifiers = try sortedIdentifiers(allocator, parsed.identifiers);        for (try view.allNodes(graph, allocator, null)) |node| {            if (!glob(node.name, selector)) continue;            if (rule.scope) |scope| if (!inScope(node.name, scope)) continue;            for (identifiers) |identifier| {                if (try metricValidationMessage(allocator, node, identifier)) |message| return message;            }        }    }    return null;}const Parsed = struct {    source: []const u8,    identifiers: []const []const u8,    expr: *Expr,};const Expr = union(enum) {    number: f64,    boolean: bool,    string: []const u8,    ident: []const u8,    unary: UnaryExpr,    binary: BinaryExpr,    compare: BinaryExpr,};const UnaryExpr = struct {    op: []const u8,    expr: *Expr,};const BinaryExpr = struct {    op: []const u8,    left: *Expr,    right: *Expr,};const TokenKind = enum {    ident,    number,    string,    op,    lparen,    rparen,    eof,};const Token = struct {    kind: TokenKind,    text: []const u8,};fn parseAssertion(allocator: std.mem.Allocator, source: []const u8) anyerror!Parsed {    const tokens = try tokenizeAssertion(allocator, source);    var state: AssertionParseState = .{ .allocator = allocator, .tokens = tokens };    const expr = try parseAssertionExpression(&state);    if (peekAssertionToken(&state).kind != .eof) return error.UnsupportedAssertion;    var identifiers: std.ArrayList([]const u8) = .empty;    try collectIdentifiers(allocator, expr, &identifiers);    return .{ .source = source, .identifiers = try identifiers.toOwnedSlice(allocator), .expr = expr };}fn validateMetrics(identifiers: []const []const u8, rule_name: []const u8) !void {    for (identifiers) |identifier| {        if (!knownMetric(identifier)) {            _ = rule_name;            return error.UnknownMetric;        }    }}fn evaluateAssertion(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, parsed: Parsed, limits: limits_mod.Analysis) !bool {    const value = try evalExpr(allocator, graph, node, parsed.expr, limits);    return switch (value) {        .boolean => |boolean| boolean,        else => error.AssertionNotBoolean,    };}const AssertionParseState = struct {    allocator: std.mem.Allocator,    tokens: []const Token,    index: usize = 0,};fn peekAssertionToken(state: *AssertionParseState) Token {    return state.tokens[state.index];}fn consumeAssertionToken(state: *AssertionParseState) Token {    const token = peekAssertionToken(state);    if (state.index + 1 < state.tokens.len) state.index += 1;    return token;}fn parseAssertionExpression(state: *AssertionParseState) anyerror!*Expr {    return try parseAssertionOr(state);}fn parseAssertionOr(state: *AssertionParseState) anyerror!*Expr {    var left = try parseAssertionAnd(state);    while (tokenEquals(peekAssertionToken(state), "or")) {        const op = consumeAssertionToken(state).text;        const right = try parseAssertionAnd(state);        left = try exprNode(state.allocator, .{ .binary = .{ .op = op, .left = left, .right = right } });    }    return left;}fn parseAssertionAnd(state: *AssertionParseState) anyerror!*Expr {    var left = try parseAssertionNot(state);    while (tokenEquals(peekAssertionToken(state), "and")) {        const op = consumeAssertionToken(state).text;        const right = try parseAssertionNot(state);        left = try exprNode(state.allocator, .{ .binary = .{ .op = op, .left = left, .right = right } });    }    return left;}fn parseAssertionNot(state: *AssertionParseState) anyerror!*Expr {    if (tokenEquals(peekAssertionToken(state), "not")) {        const op = consumeAssertionToken(state).text;        return try exprNode(state.allocator, .{ .unary = .{ .op = op, .expr = try parseAssertionNot(state) } });    }    return try parseAssertionCompare(state);}fn parseAssertionCompare(state: *AssertionParseState) anyerror!*Expr {    const left = try parseAssertionAdd(state);    if (!isCompareToken(peekAssertionToken(state))) return left;    const op = consumeAssertionToken(state).text;    const right = try parseAssertionAdd(state);    if (isCompareToken(peekAssertionToken(state))) return error.ChainedComparison;    return try exprNode(state.allocator, .{ .compare = .{ .op = op, .left = left, .right = right } });}fn parseAssertionAdd(state: *AssertionParseState) anyerror!*Expr {    var left = try parseAssertionMul(state);    while (tokenEquals(peekAssertionToken(state), "+") or tokenEquals(peekAssertionToken(state), "-")) {        const op = consumeAssertionToken(state).text;        const right = try parseAssertionMul(state);        left = try exprNode(state.allocator, .{ .binary = .{ .op = op, .left = left, .right = right } });    }    return left;}fn parseAssertionMul(state: *AssertionParseState) anyerror!*Expr {    var left = try parseAssertionUnary(state);    while (tokenEquals(peekAssertionToken(state), "*") or tokenEquals(peekAssertionToken(state), "/")) {        const op = consumeAssertionToken(state).text;        const right = try parseAssertionUnary(state);        left = try exprNode(state.allocator, .{ .binary = .{ .op = op, .left = left, .right = right } });    }    return left;}fn parseAssertionUnary(state: *AssertionParseState) anyerror!*Expr {    if (tokenEquals(peekAssertionToken(state), "+") or tokenEquals(peekAssertionToken(state), "-")) {        const op = consumeAssertionToken(state).text;        return try exprNode(state.allocator, .{ .unary = .{ .op = op, .expr = try parseAssertionUnary(state) } });    }    return try parseAssertionPrimary(state);}fn parseAssertionPrimary(state: *AssertionParseState) anyerror!*Expr {    const token = consumeAssertionToken(state);    return switch (token.kind) {        .number => try exprNode(state.allocator, .{ .number = try std.fmt.parseFloat(f64, token.text) }),        .string => try exprNode(state.allocator, .{ .string = token.text }),        .ident => {            if (peekAssertionToken(state).kind == .lparen) return error.UnsupportedCall;            if (std.mem.eql(u8, token.text, "True")) return try exprNode(state.allocator, .{ .boolean = true });            if (std.mem.eql(u8, token.text, "False")) return try exprNode(state.allocator, .{ .boolean = false });            return try exprNode(state.allocator, .{ .ident = token.text });        },        .lparen => {            const expr = try parseAssertionExpression(state);            if (peekAssertionToken(state).kind != .rparen) return error.UnsupportedAssertion;            _ = consumeAssertionToken(state);            return expr;        },        else => error.UnsupportedAssertion,    };}const ExprValue = union(enum) {    number: f64,    boolean: bool,    string: []const u8,};fn evalExpr(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, expr: *Expr, limits: limits_mod.Analysis) anyerror!ExprValue {    return switch (expr.*) {        .number => |number| .{ .number = number },        .boolean => |boolean| .{ .boolean = boolean },        .string => |string| .{ .string = string },        .ident => |ident| try metricExprValue(allocator, graph, node, ident, limits),        .unary => |unary| try evalUnary(allocator, graph, node, unary, limits),        .binary => |binary| try evalBinary(allocator, graph, node, binary, limits),        .compare => |compare_expr| .{ .boolean = try compareExprValues(try evalExpr(allocator, graph, node, compare_expr.left, limits), compare_expr.op, try evalExpr(allocator, graph, node, compare_expr.right, limits)) },    };}fn evalUnary(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, unary: UnaryExpr, limits: limits_mod.Analysis) anyerror!ExprValue {    const value = try evalExpr(allocator, graph, node, unary.expr, limits);    if (std.mem.eql(u8, unary.op, "not")) return .{ .boolean = !truthy(value) };    if (std.mem.eql(u8, unary.op, "+")) return .{ .number = try numberValue(value) };    if (std.mem.eql(u8, unary.op, "-")) return .{ .number = -(try numberValue(value)) };    return error.UnsupportedAssertion;}fn evalBinary(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, binary: BinaryExpr, limits: limits_mod.Analysis) anyerror!ExprValue {    if (std.mem.eql(u8, binary.op, "and")) {        const left = truthy(try evalExpr(allocator, graph, node, binary.left, limits));        const right = truthy(try evalExpr(allocator, graph, node, binary.right, limits));        return .{ .boolean = left and right };    }    if (std.mem.eql(u8, binary.op, "or")) {        const left = truthy(try evalExpr(allocator, graph, node, binary.left, limits));        const right = truthy(try evalExpr(allocator, graph, node, binary.right, limits));        return .{ .boolean = left or right };    }    const left = try numberValue(try evalExpr(allocator, graph, node, binary.left, limits));    const right = try numberValue(try evalExpr(allocator, graph, node, binary.right, limits));    if (std.mem.eql(u8, binary.op, "+")) return .{ .number = left + right };    if (std.mem.eql(u8, binary.op, "-")) return .{ .number = left - right };    if (std.mem.eql(u8, binary.op, "*")) return .{ .number = left * right };    if (std.mem.eql(u8, binary.op, "/")) return .{ .number = left / right };    return error.UnsupportedAssertion;}fn metricExprValue(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, metric: []const u8, limits: limits_mod.Analysis) anyerror!ExprValue {    if (std.mem.eql(u8, metric, "dead") or std.mem.eql(u8, metric, "in_cycle")) return .{ .boolean = try metricBool(allocator, graph, node, metric, limits) };    return .{ .number = try metricValue(allocator, graph, node, metric, limits) };}fn compareExprValues(left: ExprValue, op: []const u8, right: ExprValue) anyerror!bool {    if (left == .string or right == .string) {        const l = switch (left) {            .string => |value| value,            else => return error.UnsupportedAssertion,        };        const r = switch (right) {            .string => |value| value,            else => return error.UnsupportedAssertion,        };        if (std.mem.eql(u8, op, "==")) return std.mem.eql(u8, l, r);        if (std.mem.eql(u8, op, "!=")) return !std.mem.eql(u8, l, r);        return error.UnsupportedAssertion;    }    const l = try numberValue(left);    const r = try numberValue(right);    return compare(l, op, r);}fn truthy(value: ExprValue) bool {    return switch (value) {        .number => |number| number != 0,        .boolean => |boolean| boolean,        .string => |string| string.len != 0,    };}fn numberValue(value: ExprValue) anyerror!f64 {    return switch (value) {        .number => |number| number,        .boolean => |boolean| if (boolean) 1 else 0,        .string => error.UnsupportedAssertion,    };}fn exprNode(allocator: std.mem.Allocator, expr: Expr) anyerror!*Expr {    const node = try allocator.create(Expr);    node.* = expr;    return node;}fn tokenizeAssertion(allocator: std.mem.Allocator, source: []const u8) anyerror![]const Token {    var tokens: std.ArrayList(Token) = .empty;    var index: usize = 0;    while (index < source.len) {        const byte = source[index];        if (std.ascii.isWhitespace(byte)) {            index += 1;            continue;        }        if (std.ascii.isAlphabetic(byte) or byte == '_') {            const start = index;            index += 1;            while (index < source.len and (std.ascii.isAlphanumeric(source[index]) or source[index] == '_')) index += 1;            try tokens.append(allocator, .{ .kind = .ident, .text = source[start..index] });            continue;        }        if (std.ascii.isDigit(byte) or (byte == '.' and index + 1 < source.len and std.ascii.isDigit(source[index + 1]))) {            const start = index;            var seen_dot = byte == '.';            index += 1;            while (index < source.len) {                if (std.ascii.isDigit(source[index])) {                    index += 1;                } else if (source[index] == '.' and !seen_dot) {                    seen_dot = true;                    index += 1;                } else {                    break;                }            }            try tokens.append(allocator, .{ .kind = .number, .text = source[start..index] });            continue;        }        if (byte == '\'' or byte == '"') {            const quote = byte;            const start = index + 1;            index += 1;            while (index < source.len and source[index] != quote) index += 1;            if (index >= source.len) return error.UnsupportedAssertion;            try tokens.append(allocator, .{ .kind = .string, .text = source[start..index] });            index += 1;            continue;        }        if (byte == '(') {            try tokens.append(allocator, .{ .kind = .lparen, .text = source[index .. index + 1] });            index += 1;            continue;        }        if (byte == ')') {            try tokens.append(allocator, .{ .kind = .rparen, .text = source[index .. index + 1] });            index += 1;            continue;        }        if (index + 1 < source.len) {            const pair = source[index .. index + 2];            if (std.mem.eql(u8, pair, "<=") or std.mem.eql(u8, pair, ">=") or std.mem.eql(u8, pair, "==") or std.mem.eql(u8, pair, "!=")) {                try tokens.append(allocator, .{ .kind = .op, .text = pair });                index += 2;                continue;            }        }        if (byte == '+' or byte == '-' or byte == '*' or byte == '/' or byte == '<' or byte == '>') {            try tokens.append(allocator, .{ .kind = .op, .text = source[index .. index + 1] });            index += 1;            continue;        }        return error.UnsupportedAssertion;    }    try tokens.append(allocator, .{ .kind = .eof, .text = "" });    return try tokens.toOwnedSlice(allocator);}fn collectIdentifiers(allocator: std.mem.Allocator, expr: *Expr, out: *std.ArrayList([]const u8)) anyerror!void {    switch (expr.*) {        .ident => |ident| try appendIdentifier(allocator, out, ident),        .unary => |unary| try collectIdentifiers(allocator, unary.expr, out),        .binary => |binary| {            try collectIdentifiers(allocator, binary.left, out);            try collectIdentifiers(allocator, binary.right, out);        },        .compare => |compare_expr| {            try collectIdentifiers(allocator, compare_expr.left, out);            try collectIdentifiers(allocator, compare_expr.right, out);        },        else => {},    }}fn appendIdentifier(allocator: std.mem.Allocator, out: *std.ArrayList([]const u8), ident: []const u8) anyerror!void {    for (out.items) |existing| if (std.mem.eql(u8, existing, ident)) return;    try out.append(allocator, ident);}fn tokenEquals(token: Token, value: []const u8) bool {    return std.mem.eql(u8, token.text, value);}fn isCompareToken(token: Token) bool {    if (token.kind != .op) return false;    return std.mem.eql(u8, token.text, "<=") or std.mem.eql(u8, token.text, "<") or std.mem.eql(u8, token.text, ">=") or std.mem.eql(u8, token.text, ">") or std.mem.eql(u8, token.text, "==") or std.mem.eql(u8, token.text, "!=");}pub fn assertionErrorMessage(allocator: std.mem.Allocator, assertion: []const u8, err: anyerror) !?[]const u8 {    return switch (err) {        error.ChainedComparison => try allocator.dupe(u8, "chained comparisons are not supported in quantified assertions"),        error.UnsupportedCall => try unsupportedCallMessage(allocator, assertion),        error.UnsupportedAssertion => try std.fmt.allocPrint(allocator, "invalid assertion: '{s}'", .{assertion}),        error.AssertionNotBoolean => try std.fmt.allocPrint(allocator, "quantified rule assertion '{s}' did not evaluate to a boolean", .{assertion}),        else => null,    };}pub fn checkErrorMessage(allocator: std.mem.Allocator, rule: Rule, err: anyerror) !?[]const u8 {    if (std.mem.eql(u8, rule.type, "quantified")) {        if (err == error.AssertionNotBoolean) return try std.fmt.allocPrint(allocator, "quantified rule '{s}' did not evaluate to a boolean", .{rule.name});        if (rule.assertion) |assertion| {            return try assertionErrorMessage(allocator, assertion, err);        }    }    return null;}fn unsupportedCallMessage(allocator: std.mem.Allocator, assertion: []const u8) ![]const u8 {    const open = std.mem.indexOfScalar(u8, assertion, '(') orelse return try allocator.dupe(u8, "unsupported syntax in assertion");    const name = text.trim(assertion[0..open]);    return try std.fmt.allocPrint(allocator, "unsupported syntax in assertion: Call(func=Name(id='{s}', ctx=Load()))", .{name});}fn metricValue(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, metric: []const u8, limits: limits_mod.Analysis) !f64 {    if (try analysis.totalMetricValue(allocator, graph, node.name, metric, limits)) |value| return value;    if (try analysis.totalMetricBool(allocator, graph, node.name, metric)) |value| return if (value) 1 else 0;    if (isNodeMetric(metric)) return metricFromMetadata(allocator, node, metric) orelse error.MetricNotDefined;    if (isClassMetric(metric)) {        if (!std.mem.eql(u8, node.type, model.NodeType.class)) return error.MetricNotDefined;        return (try analysis.classMetricValue(allocator, graph, node.name, metric)) orelse error.MetricNotDefined;    }    if (isModuleMetric(metric)) {        if (!std.mem.eql(u8, node.type, model.NodeType.module) and !std.mem.eql(u8, node.type, model.NodeType.package)) return error.MetricNotDefined;        return (try analysis.moduleMetricValue(allocator, graph, node.name, metric)) orelse error.MetricNotDefined;    }    return error.UnknownMetric;}fn metricBool(allocator: std.mem.Allocator, graph: graph_mod.Graph, node: model.Node, metric: []const u8, limits: limits_mod.Analysis) !bool {    if (try analysis.totalMetricBool(allocator, graph, node.name, metric)) |value| return value;    return (try metricValue(allocator, graph, node, metric, limits)) != 0;}fn metricFromMetadata(allocator: std.mem.Allocator, node: model.Node, metric: []const u8) ?f64 {    const raw = model.pairValue(node.metadata, "metrics") orelse return null;    var parsed = std.json.parseFromSlice(std.json.Value, allocator, raw, .{}) catch return null;    defer parsed.deinit();    const object = switch (parsed.value) {        .object => |object| object,        else => return null,    };    const key = if (std.mem.eql(u8, metric, "nesting")) "max_nesting_depth" else metric;    const value = object.get(key) orelse return null;    return switch (value) {        .integer => |integer| @floatFromInt(integer),        .float => |float| float,        else => null,    };}fn metricValidationMessage(allocator: std.mem.Allocator, node: model.Node, metric: []const u8) !?[]const u8 {    if (isTotalMetric(metric)) return null;    if (isNodeMetric(metric)) {        if (metricFromMetadata(allocator, node, metric) != null) return null;        return try std.fmt.allocPrint(allocator, "quantified rule metric '{s}' is not defined for subject '{s}'", .{ metric, node.name });    }    if (isClassMetric(metric)) {        if (std.mem.eql(u8, node.type, model.NodeType.class)) return null;        return try std.fmt.allocPrint(allocator, "quantified rule metric '{s}' is not defined for non-class subject '{s}'", .{ metric, node.name });    }    if (isModuleMetric(metric)) {        if (std.mem.eql(u8, node.type, model.NodeType.module) or std.mem.eql(u8, node.type, model.NodeType.package)) return null;        return try std.fmt.allocPrint(allocator, "quantified rule metric '{s}' is not defined for non-module subject '{s}'", .{ metric, node.name });    }    return try std.fmt.allocPrint(allocator, "unknown metric identifier: '{s}'", .{metric});}fn sortedIdentifiers(allocator: std.mem.Allocator, identifiers: []const []const u8) ![]const []const u8 {    const sorted = try allocator.alloc([]const u8, identifiers.len);    for (identifiers, 0..) |identifier, index| sorted[index] = identifier;    std.mem.sort([]const u8, sorted, {}, cmpString);    return sorted;}fn compare(left: f64, op: []const u8, right: f64) bool {    if (std.mem.eql(u8, op, "<=")) return left <= right;    if (std.mem.eql(u8, op, "<")) return left < right;    if (std.mem.eql(u8, op, ">=")) return left >= right;    if (std.mem.eql(u8, op, ">")) return left > right;    if (std.mem.eql(u8, op, "==")) return left == right;    if (std.mem.eql(u8, op, "!=")) return left != right;    return false;}fn paramsFromObject(allocator: std.mem.Allocator, maybe: ?std.json.Value) ![]const model.Pair {    const value = maybe orelse return &.{};    const object = switch (value) {        .object => |object| object,        else => return &.{},    };    var out: std.ArrayList(model.Pair) = .empty;    var it = object.iterator();    while (it.next()) |entry| {        const rendered = switch (entry.value_ptr.*) {            .string => |string| try allocator.dupe(u8, string),            else => try pretty_json.renderMinifiedAlloc(allocator, entry.value_ptr.*),        };        try out.append(allocator, .{ .key = try allocator.dupe(u8, entry.key_ptr.*), .value = rendered, .json = entry.value_ptr.* != .string });    }    return try out.toOwnedSlice(allocator);}fn stringField(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {    const value = object.get(key) orelse return null;    return switch (value) {        .string => |string| string,        else => null,    };}fn boolField(object: std.json.ObjectMap, key: []const u8) ?bool {    const value = object.get(key) orelse return null;    return switch (value) {        .bool => |boolean| boolean,        else => null,    };}fn conceptName(concepts: []const concepts_mod.Concept, node: []const u8) ?[]const u8 {    for (concepts) |concept| {        for (concept.prefixes) |prefix| if (inScope(node, prefix)) return concept.name;    }    return null;}fn inScope(name: []const u8, scope: []const u8) bool {    if (std.mem.eql(u8, name, scope)) return true;    return std.mem.startsWith(u8, name, scope) and name.len > scope.len and name[scope.len] == '.';}fn glob(value: []const u8, pattern: []const u8) bool {    if (std.mem.eql(u8, pattern, "*")) return true;    if (std.mem.startsWith(u8, pattern, "*") and std.mem.endsWith(u8, pattern, "*")) return std.mem.indexOf(u8, value, pattern[1 .. pattern.len - 1]) != null;    if (std.mem.startsWith(u8, pattern, "*")) return std.mem.endsWith(u8, value, pattern[1..]);    if (std.mem.endsWith(u8, pattern, "*")) return std.mem.startsWith(u8, value, pattern[0 .. pattern.len - 1]);    return std.mem.eql(u8, value, pattern);}fn knownMetric(metric: []const u8) bool {    return isTotalMetric(metric) or isNodeMetric(metric) or isClassMetric(metric) or isModuleMetric(metric);}fn isTotalMetric(metric: []const u8) bool {    const metrics = [_][]const u8{ "fan_in", "fan_out", "layer", "pagerank", "betweenness", "kcore", "dead", "in_cycle" };    for (metrics) |item| if (std.mem.eql(u8, metric, item)) return true;    return false;}fn isNodeMetric(metric: []const u8) bool {    const metrics = [_][]const u8{ "cyclomatic_complexity", "cognitive_complexity", "nesting" };    for (metrics) |item| if (std.mem.eql(u8, metric, item)) return true;    return false;}fn isClassMetric(metric: []const u8) bool {    const metrics = [_][]const u8{ "wmc", "cbo", "rfc", "lcom4", "dit", "noc", "max_method_cc" };    for (metrics) |item| if (std.mem.eql(u8, metric, item)) return true;    return false;}fn isModuleMetric(metric: []const u8) bool {    const metrics = [_][]const u8{ "instability", "abstractness", "distance" };    for (metrics) |item| if (std.mem.eql(u8, metric, item)) return true;    return false;}fn isFloatMetric(metric: []const u8) bool {    return isModuleMetric(metric) or std.mem.eql(u8, metric, "pagerank") or std.mem.eql(u8, metric, "betweenness");}fn cmpRule(_: void, a: Rule, b: Rule) bool {    return std.mem.lessThan(u8, a.name, b.name);}fn cmpString(_: void, a: []const u8, b: []const u8) bool {    return std.mem.lessThan(u8, a, b);}fn modelEdgeFindingPlanBytes(limits: EdgeFindingPlan.Limits) ?usize {    if (limits.edges > std.math.maxInt(usize) / @sizeOf(model.Edge)) {        return null;    }    return limits.edges * @sizeOf(model.Edge);}test "edge finding plan capacity matches an independent typed model" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(EdgeFindingPlan, "smg_edge_finding_plan_capacity_capacity_model"),            null,            null,            null,            null,            null,            null,        );    }    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(EdgeFindingPlan, "smg_edge_finding_plan_capacity_overload"),            null,            null,            null,            null,            null,            null,        );    }    for (0..64) |count| {        const limits = EdgeFindingPlan.Limits{ .edges = count };        const capacity = try EdgeFindingPlan.Capacity.derive(limits);        try std.testing.expectEqual(modelEdgeFindingPlanBytes(limits).?, capacity.bytes);        try std.testing.expectEqual(count, capacity.edges);    }    const overflow = EdgeFindingPlan.Limits{ .edges = std.math.maxInt(usize) };    try std.testing.expect(modelEdgeFindingPlanBytes(overflow) == null);    try std.testing.expectError(        error.CapacityOverflow,        EdgeFindingPlan.Capacity.derive(overflow),    );}fn checkEdgeFindingPlanInitAllocationFailures(allocator: std.mem.Allocator) !void {    var plan = try EdgeFindingPlan.init(allocator, .{ .edges = 3 });    plan.deinit(allocator);}test "edge finding plan initialization cleans allocation failure and retries" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(EdgeFindingPlan, "smg_edge_finding_plan_oom"),            null,            null,            null,            null,            null,            null,        );    }    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkEdgeFindingPlanInitAllocationFailures,        .{},    );    var plan = try EdgeFindingPlan.init(std.testing.allocator, .{ .edges = 1 });    defer plan.deinit(std.testing.allocator);    plan.activate();    try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, plan.phase);}test "edge finding plan fills exact storage while sealed" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(EdgeFindingPlan, "smg_edge_finding_plan_sealed"),            null,            null,            null,            null,            null,            null,        );    }    var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(        std.testing.allocator,    );    var plan = try EdgeFindingPlan.init(        phase_allocator.initializationAllocator(),        .{ .edges = 2 },    );    defer {        if (phase_allocator.phase() == .initialization) {            phase_allocator.abortInitialization();        }        if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();        plan.deinit(phase_allocator.teardownAllocator());        phase_allocator.deinit();    }    const pointer = plan.edges.ptr;    phase_allocator.seal();    plan.activate();    plan.append(.{ .source = "z", .rel = "calls", .target = "a" });    plan.append(.{ .source = "a", .rel = "calls", .target = "z" });    plan.sort();    try std.testing.expectEqual(pointer, plan.edges.ptr);    try std.testing.expectEqualStrings("a", plan.edges[0].source);    try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());}fn modelNodeFindingPlanBytes(limits: NodeFindingPlan.Limits) ?usize {    if (limits.nodes > std.math.maxInt(usize) / @sizeOf([]const u8)) {        return null;    }    return limits.nodes * @sizeOf([]const u8);}test "node finding plan capacity matches an independent typed model" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(NodeFindingPlan, "smg_node_finding_plan_capacity_capacity_model"),            null,            null,            null,            null,            null,            null,        );    }    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(NodeFindingPlan, "smg_node_finding_plan_capacity_overload"),            null,            null,            null,            null,            null,            null,        );    }    for (0..64) |count| {        const limits = NodeFindingPlan.Limits{ .nodes = count };        const capacity = try NodeFindingPlan.Capacity.derive(limits);        try std.testing.expectEqual(modelNodeFindingPlanBytes(limits).?, capacity.bytes);        try std.testing.expectEqual(count, capacity.nodes);    }    const overflow = NodeFindingPlan.Limits{ .nodes = std.math.maxInt(usize) };    try std.testing.expect(modelNodeFindingPlanBytes(overflow) == null);    try std.testing.expectError(        error.CapacityOverflow,        NodeFindingPlan.Capacity.derive(overflow),    );}fn checkNodeFindingPlanInitAllocationFailures(allocator: std.mem.Allocator) !void {    var plan = try NodeFindingPlan.init(allocator, .{ .nodes = 3 });    plan.deinit(allocator);}test "node finding plan initialization cleans allocation failure and retries" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(NodeFindingPlan, "smg_node_finding_plan_oom"),            null,            null,            null,            null,            null,            null,        );    }    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkNodeFindingPlanInitAllocationFailures,        .{},    );    var plan = try NodeFindingPlan.init(std.testing.allocator, .{ .nodes = 1 });    defer plan.deinit(std.testing.allocator);    plan.activate();    try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, plan.phase);}test "node finding plan fills exact storage while sealed" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(NodeFindingPlan, "smg_node_finding_plan_sealed"),            null,            null,            null,            null,            null,            null,        );    }    var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(        std.testing.allocator,    );    var plan = try NodeFindingPlan.init(        phase_allocator.initializationAllocator(),        .{ .nodes = 2 },    );    defer {        if (phase_allocator.phase() == .initialization) {            phase_allocator.abortInitialization();        }        if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();        plan.deinit(phase_allocator.teardownAllocator());        phase_allocator.deinit();    }    const pointer = plan.nodes.ptr;    phase_allocator.seal();    plan.activate();    plan.append("z");    plan.append("a");    plan.sort();    try std.testing.expectEqual(pointer, plan.nodes.ptr);    try std.testing.expectEqualStrings("a", plan.nodes[0]);    try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());}test "quantified rules use class and module metric values" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{ .name = "mod", .type = model.NodeType.module });    try graph_mod.addNode(&graph, .{ .name = "mod.C", .type = model.NodeType.class });    const method_a_metrics = [_]model.Pair{.{ .key = "metrics", .value = "{\"cyclomatic_complexity\":3}", .json = true }};    const method_b_metrics = [_]model.Pair{.{ .key = "metrics", .value = "{\"cyclomatic_complexity\":4}", .json = true }};    try graph_mod.addNode(&graph, .{ .name = "mod.C.a", .type = model.NodeType.method, .metadata = &method_a_metrics });    try graph_mod.addNode(&graph, .{ .name = "mod.C.b", .type = model.NodeType.method, .metadata = &method_b_metrics });    try graph_mod.addEdge(&graph, .{ .source = "mod", .rel = model.RelType.contains, .target = "mod.C" });    try graph_mod.addEdge(&graph, .{ .source = "mod.C", .rel = model.RelType.contains, .target = "mod.C.a" });    try graph_mod.addEdge(&graph, .{ .source = "mod.C", .rel = model.RelType.contains, .target = "mod.C.b" });    try graph_mod.addNode(&graph, .{ .name = "app", .type = model.NodeType.module });    try graph_mod.addNode(&graph, .{ .name = "core", .type = model.NodeType.module });    try graph_mod.addEdge(&graph, .{ .source = "app", .rel = model.RelType.imports, .target = "core" });    const class_rule: Rule = .{ .name = "class-budget", .type = "quantified", .selector = "mod.C", .assertion = "wmc <= 5" };    const class_finding = (try checkRule(allocator, graph, class_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqualStrings("mod.C", class_finding.predicates[0].subject);    try std.testing.expectEqualStrings("wmc", class_finding.predicates[0].facts[0].name);    try std.testing.expectApproxEqAbs(@as(f64, 7), class_finding.predicates[0].facts[0].value.number, 0.001);    const method_rule: Rule = .{ .name = "method-budget", .type = "quantified", .selector = "mod.C", .assertion = "max_method_cc <= 3" };    const method_finding = (try checkRule(allocator, graph, method_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqualStrings("max_method_cc", method_finding.predicates[0].facts[0].name);    try std.testing.expectApproxEqAbs(@as(f64, 4), method_finding.predicates[0].facts[0].value.number, 0.001);    const module_rule: Rule = .{ .name = "module-stability", .type = "quantified", .selector = "app", .assertion = "instability <= 0.5" };    const module_finding = (try checkRule(allocator, graph, module_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqualStrings("app", module_finding.predicates[0].subject);    try std.testing.expectEqualStrings("instability", module_finding.predicates[0].facts[0].name);    try std.testing.expectApproxEqAbs(@as(f64, 1), module_finding.predicates[0].facts[0].value.float_number, 0.001);}test "quantified metric validation message matches python category checks" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{ .name = "api.handler", .type = model.NodeType.function });    const class_rule = [_]Rule{.{ .name = "class-budget", .type = "quantified", .selector = "api.*", .assertion = "wmc <= 10" }};    try std.testing.expectEqualStrings("quantified rule metric 'wmc' is not defined for non-class subject 'api.handler'", (try quantifiedMetricValidationMessage(allocator, graph, &class_rule)).?);    const node_rule = [_]Rule{.{ .name = "node-budget", .type = "quantified", .selector = "api.*", .assertion = "cyclomatic_complexity <= 10" }};    try std.testing.expectEqualStrings("quantified rule metric 'cyclomatic_complexity' is not defined for subject 'api.handler'", (try quantifiedMetricValidationMessage(allocator, graph, &node_rule)).?);    const module_rule = [_]Rule{.{ .name = "module-budget", .type = "quantified", .selector = "api.*", .assertion = "instability <= 1" }};    try std.testing.expectEqualStrings("quantified rule metric 'instability' is not defined for non-module subject 'api.handler'", (try quantifiedMetricValidationMessage(allocator, graph, &module_rule)).?);}test "deny parser and rule json use python schema" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const parsed = try parseDenyPattern("  core.*   -[imports]->   ui.*  ");    try std.testing.expectEqualStrings("core.*", parsed.source);    try std.testing.expectEqualStrings("imports", parsed.rel.?);    try std.testing.expectEqualStrings("ui.*", parsed.target);    const rule: Rule = .{ .name = "fan-out", .type = "quantified", .selector = "*", .assertion = "fan_out <= 5" };    const json = try render(allocator, rule);    try std.testing.expect(std.mem.indexOf(u8, json, "\"kind\":\"rule\"") != null);    try std.testing.expect(std.mem.indexOf(u8, json, "\"selector\":\"*\"") != null);    const params = try allocator.alloc(model.Pair, 1);    params[0] = .{ .key = "entry_points", .value = "main,cli.*" };    const invariant: Rule = .{ .name = "live", .type = "invariant", .invariant = "no-dead-code", .params = params };    try std.testing.expectEqualStrings("{\"kind\":\"rule\",\"name\":\"live\",\"type\":\"invariant\",\"invariant\":\"no-dead-code\",\"params\":{\"entry_points\":\"main,cli.*\"}}", try render(allocator, invariant));    const excluded: Rule = .{        .name = "no-back-root",        .type = "deny",        .pattern = "pkg.feature.* -[imports]-> pkg.feature.root",        .exclude_source = "*.test",        .allow_empty_target = true,    };    try std.testing.expectEqualStrings(        "{\"kind\":\"rule\",\"name\":\"no-back-root\",\"type\":\"deny\"," ++            "\"pattern\":\"pkg.feature.* -[imports]-> pkg.feature.root\"," ++            "\"exclude_source\":\"*.test\",\"allow_empty_target\":true}",        try render(allocator, excluded),    );}test "namespace import baseline update preserves rule identity and other parameters" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const params = [_]model.Pair{.{ .key = "owner", .value = "architecture" }};    const values = [_]Rule{        .{            .name = "root-handles",            .type = "invariant",            .invariant = "namespace-handle-imports",            .params = &params,        },        .{ .name = "acyclic", .type = "invariant", .invariant = "no-cycles" },    };    const first = try setNamespaceHandleImportBaseline(        allocator,        &values,        "root-handles",        "1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",    );    try std.testing.expectEqual(@as(usize, 2), first.len);    try std.testing.expectEqualStrings("architecture", model.pairValue(first[0].params, "owner").?);    try std.testing.expectEqualStrings(        "1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",        model.pairValue(first[0].params, "baseline").?,    );    const second = try setNamespaceHandleImportBaseline(        allocator,        first,        "root-handles",        "0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",    );    try std.testing.expectEqual(@as(usize, 2), second[0].params.len);    try std.testing.expectEqualStrings(        "0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",        model.pairValue(second[0].params, "baseline").?,    );    try std.testing.expectError(        error.InvalidRule,        setNamespaceHandleImportBaseline(            allocator,            &values,            "acyclic",            "0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",        ),    );}test "deny empty-target policy persists through the rule store" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const root = try tmp.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);    const values = [_]Rule{        .{ .name = "absence", .type = "deny", .pattern = "pkg -> *.integration", .allow_empty_target = true },        .{ .name = "ordinary", .type = "deny", .pattern = "pkg -> dep" },    };    try save(allocator, root, &values);    const loaded = try load(allocator, root, @import("root.zig").default_limits.storage);    try std.testing.expectEqual(@as(usize, 2), loaded.len);    try std.testing.expect(loaded[0].allow_empty_target);    try std.testing.expect(!loaded[1].allow_empty_target);}test "deny source exclusion preserves production edges" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    for ([_][]const u8{        "pkg.feature.impl",        "pkg.feature.root",        "pkg.feature.test",    }) |name| {        try graph_mod.addNode(&graph, .{            .name = name,            .type = model.NodeType.module,        });    }    try graph_mod.addEdge(&graph, .{        .source = "pkg.feature.impl",        .target = "pkg.feature.root",        .rel = model.RelType.imports,    });    try graph_mod.addEdge(&graph, .{        .source = "pkg.feature.test",        .target = "pkg.feature.root",        .rel = model.RelType.imports,    });    const rule: Rule = .{        .name = "no-back-root",        .type = "deny",        .pattern = "pkg.feature.* -[imports]-> pkg.feature.root",        .exclude_source = "*.test",    };    const finding = (try checkDeny(std.testing.allocator, graph, rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.edges);    try std.testing.expectEqual(@as(usize, 1), finding.edges.len);    try std.testing.expectEqualStrings("pkg.feature.impl", finding.edges[0].source);    try std.testing.expectEqualStrings("pkg.feature.root", finding.edges[0].target);}test "deny namespace boundary rejects a non-root implementation backedge" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    for ([_][]const u8{        "lib.accy.src.choir.root",        "lib.accy.src.choir.dialect",        "lib.accy.src.artifact.root",    }) |name| {        try graph_mod.addNode(&graph, .{            .name = name,            .type = model.NodeType.module,        });    }    try graph_mod.addEdge(&graph, .{        .source = "lib.accy.src.choir.dialect",        .target = "lib.accy.src.artifact.root",        .rel = model.RelType.imports,    });    const rule: Rule = .{        .name = "lib-accy-choir-no-artifact-import",        .type = "deny",        .pattern = "lib.accy.src.choir.* -[imports]-> lib.accy.src.artifact.root",    };    const finding = (try checkDeny(std.testing.allocator, graph, rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.edges);    try std.testing.expectEqual(@as(usize, 1), finding.edges.len);    try std.testing.expectEqualStrings("lib.accy.src.choir.dialect", finding.edges[0].source);}test "edge findings allocate only exact sorted offending edges" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(EdgeFindingPlan, "smg_edge_finding_plan_integration"),            null,            null,            null,            null,            null,            null,        );    }    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try graph_mod.addNode(&graph, .{ .name = "a", .type = model.NodeType.module });    try graph_mod.addNode(&graph, .{ .name = "z", .type = model.NodeType.module });    try graph_mod.addEdge(&graph, .{        .source = "a",        .target = "z",        .rel = model.RelType.contains,    });    try graph_mod.addEdge(&graph, .{        .source = "z",        .target = "a",        .rel = model.RelType.imports,    });    try graph_mod.addEdge(&graph, .{        .source = "a",        .target = "z",        .rel = model.RelType.imports,    });    const pass: Rule = .{        .name = "pass",        .type = "deny",        .pattern = "ghost.* -[imports]-> *",    };    var failing = std.testing.FailingAllocator.init(        std.testing.allocator,        .{ .fail_index = 0 },    );    try std.testing.expect(try checkDeny(failing.allocator(), graph, pass) == null);    const fail: Rule = .{        .name = "fail",        .type = "deny",        .pattern = "* -[imports]-> *",    };    const finding = (try checkDeny(std.testing.allocator, graph, fail)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.edges);    try std.testing.expectEqual(@as(usize, 2), finding.edges.len);    try std.testing.expectEqualStrings("a", finding.edges[0].source);    try std.testing.expectEqualStrings("z", finding.edges[1].source);}const file_directory_rule: Rule = .{    .name = "file-directory-collisions",    .type = "invariant",    .invariant = "file-directory-collisions",};fn addFileDirectoryModule(graph: *graph_mod.Graph, name: []const u8, file: ?[]const u8) !void {    try graph_mod.addNode(graph, .{        .name = name,        .type = model.NodeType.module,        .file = file,    });}test "file directory collisions allocate only exact sorted source modules" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(NodeFindingPlan, "smg_node_finding_plan_integration"),            null,            null,            null,            null,            null,            null,        );    }    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addFileDirectoryModule(&graph, "zeta", "zeta.zig");    try addFileDirectoryModule(&graph, "zeta.child", "zeta/child.zig");    try addFileDirectoryModule(&graph, "alpha", "alpha.zig");    try addFileDirectoryModule(&graph, "alpha.deep.child", "alpha/deep/child.zig");    try addFileDirectoryModule(&graph, "alpha.root", "alpha/root.zig");    try addFileDirectoryModule(&graph, "mixed", "mixed.zig");    try addFileDirectoryModule(&graph, "mixed.child", "mixed/child.py");    try addFileDirectoryModule(&graph, "clear", "clear.zig");    const pass: Rule = .{        .name = "clear-file-directories",        .type = "invariant",        .invariant = "file-directory-collisions",        .scope = "clear",    };    var failing = std.testing.FailingAllocator.init(        std.testing.allocator,        .{ .fail_index = 0 },    );    try std.testing.expect(try checkFileDirectoryCollisions(failing.allocator(), graph, pass) == null);    const finding = (try checkFileDirectoryCollisions(std.testing.allocator, graph, file_directory_rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.nodes);    try std.testing.expectEqualStrings("3 file-directory concept collision(s)", finding.message);    try std.testing.expectEqual(@as(usize, 3), finding.nodes.len);    try std.testing.expectEqualStrings("alpha", finding.nodes[0]);    try std.testing.expectEqualStrings("mixed", finding.nodes[1]);    try std.testing.expectEqualStrings("zeta", finding.nodes[2]);}test "file directory collisions exempt exact build and test aggregates" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addFileDirectoryModule(&graph, "pkg.build", "pkg/build.zig");    try addFileDirectoryModule(&graph, "pkg.build.step", "pkg/build/step.zig");    try addFileDirectoryModule(&graph, "pkg.test", "pkg/test.zig");    try addFileDirectoryModule(&graph, "pkg.test.case", "pkg/test/case.zig");    try addFileDirectoryModule(&graph, "pkg.builder", "pkg/builder.zig");    try addFileDirectoryModule(&graph, "pkg.builder.step", "pkg/builder/step.zig");    const finding = (try checkFileDirectoryCollisions(std.testing.allocator, graph, file_directory_rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.nodes);    try std.testing.expectEqual(@as(usize, 1), finding.nodes.len);    try std.testing.expectEqualStrings("pkg.builder", finding.nodes[0]);}test "file directory collisions ignore unresolved and path prefix lookalikes" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addFileDirectoryModule(&graph, "pkg.feature", "pkg/feature.zig");    try addFileDirectoryModule(&graph, "pkg.feature.child", null);    try addFileDirectoryModule(&graph, "pkg.featureish.child", "pkg/featureish/child.zig");    try addFileDirectoryModule(&graph, "pkg.python", "pkg/python.py");    try addFileDirectoryModule(&graph, "pkg.python.child", "pkg/python/child.zig");    var failing = std.testing.FailingAllocator.init(        std.testing.allocator,        .{ .fail_index = 0 },    );    try std.testing.expect(try checkFileDirectoryCollisions(failing.allocator(), graph, file_directory_rule) == null);}test "file directory collisions scope the source module" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addFileDirectoryModule(&graph, "pkg.first", "pkg/first.zig");    try addFileDirectoryModule(&graph, "pkg.first.child", "pkg/first/child.zig");    try addFileDirectoryModule(&graph, "pkg.second", "pkg/second.zig");    try addFileDirectoryModule(&graph, "pkg.second.child", "pkg/second/child.zig");    const rule: Rule = .{        .name = "first-file-directories",        .type = "invariant",        .invariant = "file-directory-collisions",        .scope = "pkg.first",    };    const finding = (try checkInvariant(std.testing.allocator, graph, rule, &.{}, testing_analysis_limits)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.nodes);    try std.testing.expectEqual(@as(usize, 1), finding.nodes.len);    try std.testing.expectEqualStrings("pkg.first", finding.nodes[0]);}test "parent filter admits every strictly nested directory" {    const files = [_][]const u8{        "a.zig",        "a/b.zig",        "a/b/c.zig",        "pkg//child.zig",        "/abs/x/y.zig",    };    var nodes: [files.len]model.Node = undefined;    for (files, 0..) |file, index| {        nodes[index] = .{ .name = file, .type = model.NodeType.module, .file = file };    }    var parents: ParentFilter = undefined;    parents.fill(&nodes);    for (files) |file| {        for (files) |candidate| {            var end: usize = 0;            while (end <= candidate.len) : (end += 1) {                const directory = candidate[0..end];                if (!pathIsStrictlyUnder(file, directory)) continue;                try std.testing.expect(parents.admits(directory));            }        }    }    try std.testing.expect(parents.admits(""));    var empty: ParentFilter = undefined;    empty.fill(&.{});    try std.testing.expect(!empty.admits(""));    try std.testing.expect(!pathIsStrictlyUnder("", ""));}test "file directory collisions admit separator terminated stems" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addFileDirectoryModule(&graph, "pkg.odd", "pkg//.zig");    try addFileDirectoryModule(&graph, "pkg.odd.child", "pkg//child.zig");    const finding = (try checkFileDirectoryCollisions(std.testing.allocator, graph, file_directory_rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.nodes);    try std.testing.expectEqual(@as(usize, 1), finding.nodes.len);    try std.testing.expectEqualStrings("pkg.odd", finding.nodes[0]);}const cycle_rule: Rule = .{    .name = "no-cycles",    .type = "invariant",    .invariant = "no-cycles",};fn addCycleModule(graph: *graph_mod.Graph, name: []const u8) !void {    try graph_mod.addNode(graph, .{ .name = name, .type = model.NodeType.module });}test "cycle invariant reports the first sorted coupling back edge" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    for ([_][]const u8{ "a", "b", "c" }) |name| try addCycleModule(&graph, name);    try graph_mod.addEdge(&graph, .{ .source = "a", .rel = model.RelType.contains, .target = "b" });    try graph_mod.addEdge(&graph, .{ .source = "c", .rel = model.RelType.imports, .target = "b" });    try graph_mod.addEdge(&graph, .{ .source = "b", .rel = model.RelType.imports, .target = "c" });    const finding = (try checkInvariant(allocator, graph, cycle_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqualStrings("1 cycle(s)", finding.message);    try std.testing.expectEqual(@as(usize, 1), finding.cycles.len);    try std.testing.expectEqual(@as(usize, 2), finding.cycles[0].len);    try std.testing.expectEqualStrings("b", finding.cycles[0][0]);    try std.testing.expectEqualStrings("c", finding.cycles[0][1]);}test "cycle invariant counts self coupling and ignores containment" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    for ([_][]const u8{ "a", "b" }) |name| try addCycleModule(&graph, name);    try graph_mod.addEdge(&graph, .{ .source = "a", .rel = model.RelType.imports, .target = "b" });    try graph_mod.addEdge(&graph, .{ .source = "b", .rel = model.RelType.contains, .target = "a" });    try std.testing.expect(try checkInvariant(allocator, graph, cycle_rule, &.{}, testing_analysis_limits) == null);    try graph_mod.addEdge(&graph, .{ .source = "b", .rel = model.RelType.calls, .target = "b" });    const finding = (try checkInvariant(allocator, graph, cycle_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 1), finding.cycles.len);    try std.testing.expectEqualStrings("b", finding.cycles[0][0]);    try std.testing.expectEqualStrings("b", finding.cycles[0][1]);}test "prepared check fills one parent filter and only when a rule reads it" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    try addFileDirectoryModule(&graph, "pkg.first", "pkg/first.zig");    try addFileDirectoryModule(&graph, "pkg.first.child", "pkg/first/child.zig");    try addFileDirectoryModule(&graph, "pkg.second", "pkg/second.zig");    try addFileDirectoryModule(&graph, "pkg.second.child", "pkg/second/child.zig");    const rule_list = [_]Rule{        .{ .name = "deny", .type = "deny", .pattern = "ghost.* -[imports]-> *" },        .{ .name = "first", .type = "invariant", .invariant = "file-directory-collisions", .scope = "pkg.first" },        .{ .name = "second", .type = "invariant", .invariant = "file-directory-collisions", .scope = "pkg.second" },        .{ .name = "both", .type = "invariant", .invariant = "file-directory-collisions" },    };    var prepared: Check = undefined;    prepared.prepare(graph, testing_analysis_limits);    try std.testing.expect(try prepared.rule(allocator, rule_list[0], &.{}) == null);    try std.testing.expect(!prepared.parents_ready);    const first = (try prepared.rule(allocator, rule_list[1], &.{})).?;    try std.testing.expect(prepared.parents_ready);    const words = prepared.parents.words;    const findings = try check(allocator, graph, &rule_list, null, &.{}, testing_analysis_limits);    try std.testing.expectEqual(@as(usize, 3), findings.len);    try std.testing.expectEqualStrings(first.nodes[0], findings[0].nodes[0]);    try std.testing.expectEqualStrings("pkg.second", findings[1].nodes[0]);    try std.testing.expectEqual(@as(usize, 2), findings[2].nodes.len);    try std.testing.expectEqualStrings("pkg.first", findings[2].nodes[0]);    try std.testing.expectEqualStrings("pkg.second", findings[2].nodes[1]);    prepared.parents.fill(graph.nodes.items);    try std.testing.expectEqualSlices(u64, &words, &prepared.parents.words);}const namespace_handle_rule: Rule = .{    .name = "namespace-handles",    .type = "invariant",    .invariant = "namespace-handle-imports",};fn addNamespaceModule(graph: *graph_mod.Graph, name: []const u8, file: []const u8) !void {    try graph_mod.addNode(graph, .{        .name = name,        .type = model.NodeType.module,        .file = file,    });}fn addNamespaceImport(graph: *graph_mod.Graph, source: []const u8, target: []const u8) !void {    try graph_mod.addEdge(graph, .{        .source = source,        .target = target,        .rel = model.RelType.imports,    });}test "namespace handle imports allow same owner implementations" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&graph, "pkg.deep.first", "pkg/deep/first.zig");    try addNamespaceModule(&graph, "pkg.deep.second", "pkg/deep/second.zig");    try addNamespaceImport(&graph, "pkg.deep.first", "pkg.deep.second");    var failing = std.testing.FailingAllocator.init(        std.testing.allocator,        .{ .fail_index = 0 },    );    try std.testing.expect(try checkNamespaceHandleImports(failing.allocator(), graph, namespace_handle_rule) == null);}test "namespace handle imports deny parent child and sibling implementations" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&graph, "pkg.impl", "pkg/impl.zig");    try addNamespaceModule(&graph, "pkg.child.root", "pkg/child/root.zig");    try addNamespaceModule(&graph, "pkg.child.impl", "pkg/child/impl.zig");    try addNamespaceModule(&graph, "pkg.sibling.root", "pkg/sibling/root.zig");    try addNamespaceModule(&graph, "pkg.sibling.impl", "pkg/sibling/impl.zig");    try addNamespaceImport(&graph, "pkg.child.impl", "pkg.impl");    try addNamespaceImport(&graph, "pkg.impl", "pkg.child.impl");    try addNamespaceImport(&graph, "pkg.child.impl", "pkg.sibling.impl");    const finding = (try checkNamespaceHandleImports(std.testing.allocator, graph, namespace_handle_rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.edges);    try std.testing.expectEqual(@as(usize, 3), finding.edges.len);    try std.testing.expectEqualStrings("pkg.child.impl", finding.edges[0].source);    try std.testing.expectEqualStrings("pkg.impl", finding.edges[0].target);    try std.testing.expectEqualStrings("pkg.child.impl", finding.edges[1].source);    try std.testing.expectEqualStrings("pkg.sibling.impl", finding.edges[1].target);    try std.testing.expectEqualStrings("pkg.impl", finding.edges[2].source);    try std.testing.expectEqualStrings("pkg.child.impl", finding.edges[2].target);}test "namespace handle imports allow cross owner root handles" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&graph, "pkg.impl", "pkg/impl.zig");    try addNamespaceModule(&graph, "pkg.child.root", "pkg/child/root.zig");    try addNamespaceModule(&graph, "pkg.child.impl", "pkg/child/impl.zig");    try addNamespaceImport(&graph, "pkg.impl", "pkg.child.root");    try addNamespaceImport(&graph, "pkg.child.impl", "pkg.root");    try std.testing.expect(try checkNamespaceHandleImports(std.testing.allocator, graph, namespace_handle_rule) == null);}test "namespace handle imports allow child test discovery only from tests" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&graph, "pkg.test", "pkg/test.zig");    try addNamespaceModule(&graph, "pkg.child.root", "pkg/child/root.zig");    try addNamespaceModule(&graph, "pkg.child.test", "pkg/child/test.zig");    try addNamespaceImport(&graph, "pkg.test", "pkg.child.test");    try addNamespaceImport(&graph, "pkg.root", "pkg.child.test");    const finding = (try checkNamespaceHandleImports(std.testing.allocator, graph, namespace_handle_rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.edges);    try std.testing.expectEqual(@as(usize, 1), finding.edges.len);    try std.testing.expectEqualStrings("pkg.root", finding.edges[0].source);    try std.testing.expectEqualStrings("pkg.child.test", finding.edges[0].target);}test "namespace handle imports ignore unresolved external nodes" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&graph, "pkg.impl", "pkg/impl.zig");    try graph_mod.addNode(&graph, .{        .name = "external",        .type = model.NodeType.module,    });    try addNamespaceImport(&graph, "pkg.impl", "external");    try addNamespaceImport(&graph, "external", "pkg.impl");    try std.testing.expect(try checkNamespaceHandleImports(std.testing.allocator, graph, namespace_handle_rule) == null);}test "namespace handle imports respect source scope" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&graph, "pkg.impl", "pkg/impl.zig");    try addNamespaceModule(&graph, "pkg.child.root", "pkg/child/root.zig");    try addNamespaceModule(&graph, "pkg.child.impl", "pkg/child/impl.zig");    try addNamespaceImport(&graph, "pkg.impl", "pkg.child.impl");    try addNamespaceImport(&graph, "pkg.child.impl", "pkg.impl");    const rule: Rule = .{        .name = "child-namespace-handles",        .type = "invariant",        .invariant = "namespace-handle-imports",        .scope = "pkg.child",    };    const finding = (try checkNamespaceHandleImports(std.testing.allocator, graph, rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.edges);    try std.testing.expectEqual(@as(usize, 1), finding.edges.len);    try std.testing.expectEqualStrings("pkg.child.impl", finding.edges[0].source);    try std.testing.expectEqualStrings("pkg.impl", finding.edges[0].target);}test "namespace handle import baseline detects exact edge-set drift" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&graph, "pkg.impl", "pkg/impl.zig");    try addNamespaceModule(&graph, "pkg.child.root", "pkg/child/root.zig");    try addNamespaceModule(&graph, "pkg.child.impl", "pkg/child/impl.zig");    try addNamespaceModule(&graph, "pkg.sibling.root", "pkg/sibling/root.zig");    try addNamespaceModule(&graph, "pkg.sibling.impl", "pkg/sibling/impl.zig");    try addNamespaceImport(&graph, "pkg.impl", "pkg.child.impl");    const initial = (try checkNamespaceHandleImports(std.testing.allocator, graph, namespace_handle_rule)).?;    const digest = namespaceHandleImportDigest(initial.edges);    var baseline_buffer: [96]u8 = undefined;    const baseline = try std.fmt.bufPrint(        &baseline_buffer,        "{d}:{s}",        .{ initial.edges.len, std.fmt.bytesToHex(digest, .lower) },    );    std.testing.allocator.free(initial.message);    std.testing.allocator.free(initial.edges);    const params = [_]model.Pair{.{ .key = "baseline", .value = baseline }};    const rule: Rule = .{        .name = "repository-namespace-handles",        .type = "invariant",        .invariant = "namespace-handle-imports",        .params = &params,    };    try std.testing.expect(try checkNamespaceHandleImports(std.testing.allocator, graph, rule) == null);    try addNamespaceImport(&graph, "pkg.child.impl", "pkg.sibling.impl");    const changed = (try checkNamespaceHandleImports(std.testing.allocator, graph, rule)).?;    defer std.testing.allocator.free(changed.message);    defer std.testing.allocator.free(changed.edges);    try std.testing.expectEqual(@as(usize, 2), changed.edges.len);    try std.testing.expect(std.mem.indexOf(u8, changed.message, "expected 1:") != null);    try std.testing.expect(std.mem.indexOf(u8, changed.message, "actual 2:") != null);}test "namespace handle import baseline identifies equal-count replacement" {    var initial_graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&initial_graph);    try addNamespaceModule(&initial_graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&initial_graph, "pkg.impl", "pkg/impl.zig");    try addNamespaceModule(&initial_graph, "pkg.child.root", "pkg/child/root.zig");    try addNamespaceModule(&initial_graph, "pkg.child.impl", "pkg/child/impl.zig");    try addNamespaceModule(&initial_graph, "pkg.sibling.root", "pkg/sibling/root.zig");    try addNamespaceModule(&initial_graph, "pkg.sibling.impl", "pkg/sibling/impl.zig");    try addNamespaceImport(&initial_graph, "pkg.impl", "pkg.child.impl");    const initial = (try checkNamespaceHandleImports(std.testing.allocator, initial_graph, namespace_handle_rule)).?;    const digest = namespaceHandleImportDigest(initial.edges);    var baseline_buffer: [96]u8 = undefined;    const baseline = try std.fmt.bufPrint(        &baseline_buffer,        "{d}:{s}",        .{ initial.edges.len, std.fmt.bytesToHex(digest, .lower) },    );    std.testing.allocator.free(initial.message);    std.testing.allocator.free(initial.edges);    var replacement_graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&replacement_graph);    try addNamespaceModule(&replacement_graph, "pkg.root", "pkg/root.zig");    try addNamespaceModule(&replacement_graph, "pkg.impl", "pkg/impl.zig");    try addNamespaceModule(&replacement_graph, "pkg.child.root", "pkg/child/root.zig");    try addNamespaceModule(&replacement_graph, "pkg.child.impl", "pkg/child/impl.zig");    try addNamespaceModule(&replacement_graph, "pkg.sibling.root", "pkg/sibling/root.zig");    try addNamespaceModule(&replacement_graph, "pkg.sibling.impl", "pkg/sibling/impl.zig");    try addNamespaceImport(&replacement_graph, "pkg.child.impl", "pkg.sibling.impl");    const params = [_]model.Pair{.{ .key = "baseline", .value = baseline }};    const rule: Rule = .{        .name = "repository-namespace-handles",        .type = "invariant",        .invariant = "namespace-handle-imports",        .params = &params,    };    const changed = (try checkNamespaceHandleImports(std.testing.allocator, replacement_graph, rule)).?;    defer std.testing.allocator.free(changed.message);    defer std.testing.allocator.free(changed.edges);    try std.testing.expectEqual(@as(usize, 1), changed.edges.len);    try std.testing.expect(std.mem.indexOf(u8, changed.message, "edge set replaced at unchanged count") != null);}test "namespace handle import baseline rejects stale debt" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    try addNamespaceModule(&graph, "pkg.root", "pkg/root.zig");    const params = [_]model.Pair{.{        .key = "baseline",        .value = "1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",    }};    const rule: Rule = .{        .name = "repository-namespace-handles",        .type = "invariant",        .invariant = "namespace-handle-imports",        .params = &params,    };    const finding = (try checkNamespaceHandleImports(std.testing.allocator, graph, rule)).?;    defer std.testing.allocator.free(finding.message);    defer std.testing.allocator.free(finding.edges);    try std.testing.expectEqual(@as(usize, 0), finding.edges.len);    try std.testing.expect(std.mem.indexOf(u8, finding.message, "actual 0:") != null);    try std.testing.expect(std.mem.indexOf(        u8,        finding.message,        "smg rule baseline repository-namespace-handles 0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",    ) != null);}test "namespace handle import baseline requires a complete digest" {    var graph = graph_mod.init(std.testing.allocator);    defer graph_mod.deinit(&graph);    const params = [_]model.Pair{.{ .key = "baseline", .value = "0:00" }};    const rule: Rule = .{        .name = "repository-namespace-handles",        .type = "invariant",        .invariant = "namespace-handle-imports",        .params = &params,    };    try std.testing.expectError(error.InvalidRule, checkNamespaceHandleImports(std.testing.allocator, graph, rule));}test "quantified checks preserve python predicate witnesses" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    for ([_][]const u8{ "ui.app", "db.query", "core.lib" }) |name| try graph_mod.addNode(&graph, .{ .name = name, .type = model.NodeType.function });    try graph_mod.addEdge(&graph, .{ .source = "ui.app", .target = "db.query", .rel = model.RelType.calls });    try graph_mod.addEdge(&graph, .{ .source = "core.lib", .target = "ui.app", .rel = model.RelType.calls });    const rule: Rule = .{ .name = "fan-out", .type = "quantified", .selector = "*", .assertion = "fan_out <= 0" };    const finding = (try checkRule(allocator, graph, rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 2), finding.predicates.len);    try std.testing.expectEqualStrings("core.lib", finding.predicates[0].subject);    try std.testing.expectEqualStrings("fan_out <= 0", finding.predicates[0].assertion);    try std.testing.expectEqualStrings("fan_out", finding.predicates[0].facts[0].name);    try std.testing.expectEqual(@as(f64, 1), finding.predicates[0].facts[0].value.number);    try std.testing.expectEqualStrings("ui.app", finding.predicates[1].subject);}test "quantified assertions support python expression subset" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    for ([_][]const u8{ "ui.app", "ui.app.view", "db.query", "core.lib", "util.helpers" }) |name| try graph_mod.addNode(&graph, .{ .name = name, .type = model.NodeType.function });    try graph_mod.addEdge(&graph, .{ .source = "ui.app", .target = "db.query", .rel = model.RelType.calls });    try graph_mod.addEdge(&graph, .{ .source = "ui.app", .target = "db.query", .rel = model.RelType.imports });    try graph_mod.addEdge(&graph, .{ .source = "ui.app", .target = "ui.app.view", .rel = model.RelType.contains });    try graph_mod.addEdge(&graph, .{ .source = "core.lib", .target = "util.helpers", .rel = model.RelType.imports });    const arithmetic: Rule = .{ .name = "combined-fan", .type = "quantified", .selector = "*", .assertion = "fan_out + fan_in <= 1" };    try std.testing.expect(try checkRule(allocator, graph, arithmetic, &.{}, testing_analysis_limits) == null);    const boolean_rule: Rule = .{ .name = "boolean", .type = "quantified", .selector = "*", .assertion = "fan_out <= 5 and not in_cycle" };    try std.testing.expect(try checkRule(allocator, graph, boolean_rule, &.{}, testing_analysis_limits) == null);    const unary_rule: Rule = .{ .name = "unary", .type = "quantified", .selector = "*", .assertion = "-fan_out <= 0" };    try std.testing.expect(try checkRule(allocator, graph, unary_rule, &.{}, testing_analysis_limits) == null);    const division_rule: Rule = .{ .name = "division", .type = "quantified", .selector = "*", .assertion = "fan_out / 2 < 0.5" };    const division_finding = (try checkRule(allocator, graph, division_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 2), division_finding.predicates.len);    try std.testing.expectEqualStrings("core.lib", division_finding.predicates[0].subject);    try std.testing.expectEqualStrings("ui.app", division_finding.predicates[1].subject);    const non_boolean: Rule = .{ .name = "nonbool", .type = "quantified", .selector = "*", .assertion = "fan_out + fan_in" };    try std.testing.expectError(error.AssertionNotBoolean, checkRule(allocator, graph, non_boolean, &.{}, testing_analysis_limits));    try std.testing.expectEqualStrings("quantified rule 'nonbool' did not evaluate to a boolean", (try checkErrorMessage(allocator, non_boolean, error.AssertionNotBoolean)).?);}test "quantified total metrics use analysis values" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    for ([_][]const u8{ "a", "b", "c" }) |name| try graph_mod.addNode(&graph, .{ .name = name, .type = model.NodeType.function });    try graph_mod.addEdge(&graph, .{ .source = "a", .target = "b", .rel = model.RelType.calls });    try graph_mod.addEdge(&graph, .{ .source = "b", .target = "c", .rel = model.RelType.calls });    const layer_rule: Rule = .{ .name = "layer", .type = "quantified", .selector = "*", .assertion = "layer <= 0" };    const layer_finding = (try checkRule(allocator, graph, layer_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 2), layer_finding.predicates.len);    try std.testing.expectEqualStrings("a", layer_finding.predicates[0].subject);    try std.testing.expectEqual(@as(f64, 2), layer_finding.predicates[0].facts[0].value.number);    try std.testing.expectEqualStrings("b", layer_finding.predicates[1].subject);    try std.testing.expectEqual(@as(f64, 1), layer_finding.predicates[1].facts[0].value.number);    const pagerank_rule: Rule = .{ .name = "pagerank", .type = "quantified", .selector = "*", .assertion = "pagerank == 0" };    const pagerank_finding = (try checkRule(allocator, graph, pagerank_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 3), pagerank_finding.predicates.len);    try std.testing.expectApproxEqAbs(@as(f64, 0.18441678192715533), pagerank_finding.predicates[0].facts[0].value.float_number, 0.000000000001);    const betweenness_rule: Rule = .{ .name = "between", .type = "quantified", .selector = "*", .assertion = "betweenness == 0" };    const betweenness_finding = (try checkRule(allocator, graph, betweenness_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 1), betweenness_finding.predicates.len);    try std.testing.expectEqualStrings("b", betweenness_finding.predicates[0].subject);    try std.testing.expectEqual(@as(f64, 1), betweenness_finding.predicates[0].facts[0].value.float_number);    const kcore_rule: Rule = .{ .name = "kcore", .type = "quantified", .selector = "*", .assertion = "kcore == 0" };    const kcore_finding = (try checkRule(allocator, graph, kcore_rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 3), kcore_finding.predicates.len);    try std.testing.expectEqual(@as(f64, 1), kcore_finding.predicates[0].facts[0].value.number);}test "quantified in_cycle is subject scoped" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    for ([_][]const u8{ "x", "y", "z" }) |name| try graph_mod.addNode(&graph, .{ .name = name, .type = model.NodeType.function });    try graph_mod.addEdge(&graph, .{ .source = "x", .target = "y", .rel = model.RelType.calls });    try graph_mod.addEdge(&graph, .{ .source = "y", .target = "x", .rel = model.RelType.calls });    try graph_mod.addEdge(&graph, .{ .source = "z", .target = "x", .rel = model.RelType.calls });    const rule: Rule = .{ .name = "cycle", .type = "quantified", .selector = "*", .assertion = "in_cycle == False" };    const finding = (try checkRule(allocator, graph, rule, &.{}, testing_analysis_limits)).?;    try std.testing.expectEqual(@as(usize, 2), finding.predicates.len);    try std.testing.expectEqualStrings("x", finding.predicates[0].subject);    try std.testing.expectEqualStrings("y", finding.predicates[1].subject);}test "quantified assertion parse errors match python surface" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    try std.testing.expectError(error.ChainedComparison, parseAssertion(allocator, "fan_out <= 5 <= 10"));    try std.testing.expectEqualStrings("chained comparisons are not supported in quantified assertions", (try assertionErrorMessage(allocator, "fan_out <= 5 <= 10", error.ChainedComparison)).?);    try std.testing.expectError(error.UnsupportedCall, parseAssertion(allocator, "metric()"));    try std.testing.expectEqualStrings("unsupported syntax in assertion: Call(func=Name(id='metric', ctx=Load()))", (try assertionErrorMessage(allocator, "metric()", error.UnsupportedCall)).?);}test "dead-rule pattern survey deduplicates and marks in one node pass" {    const rule_list = [_]Rule{        .{ .name = "live", .type = "deny", .pattern = "lib.* -> tools.*" },        .{ .name = "dead", .type = "deny", .pattern = "src.repl.* -> lib.*" },        .{ .name = "shared", .type = "quantified", .selector = "src.repl.*" },    };    var pattern_buffer: [rule_list.len * 2]PatternPresence = undefined;    const patterns = pattern_buffer[0..collectDeadRulePatterns(&pattern_buffer, &rule_list)];    try std.testing.expectEqual(@as(usize, 3), patterns.len);    const nodes = [_]model.Node{        .{ .name = "lib.pretty", .type = model.NodeType.module },        .{ .name = "tools.smg", .type = model.NodeType.module },    };    markPresentPatterns(patterns, &nodes);    try std.testing.expect(patternIsPresent(patterns, "lib.*"));    try std.testing.expect(patternIsPresent(patterns, "tools.*"));    try std.testing.expect(!patternIsPresent(patterns, "src.repl.*"));}test "deadRules distinguishes absence guards from missing deny targets" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{ .name = "lib.pretty", .type = model.NodeType.module });    try graph_mod.addNode(&graph, .{ .name = "tools.smg", .type = model.NodeType.module });    const live_deny: Rule = .{ .name = "lib-no-tools", .type = "deny", .pattern = "lib.* -> tools.*" };    const dead_source: Rule = .{ .name = "repl-gone", .type = "deny", .pattern = "src.repl.* -> lib.*" };    const dead_target: Rule = .{ .name = "target-gone", .type = "deny", .pattern = "lib.* -> vendor.*" };    const absence_guard: Rule = .{        .name = "vendor-stays-gone",        .type = "deny",        .pattern = "lib.* -> vendor.*",        .allow_empty_target = true,    };    const dead_guard_source: Rule = .{        .name = "repl-stays-gone",        .type = "deny",        .pattern = "src.repl.* -> vendor.*",        .allow_empty_target = true,    };    const dead_selector: Rule = .{        .name = "ghost-budget",        .type = "quantified",        .selector = "src.repl.*",        .assertion = "wmc <= 5",    };    const live_invariant: Rule = .{        .name = "acyclic",        .type = "invariant",        .invariant = "no-cycles",    };    const dead = try deadRules(allocator, graph, &.{        live_deny,        dead_source,        dead_target,        absence_guard,        dead_guard_source,        dead_selector,        live_invariant,    });    try std.testing.expectEqual(@as(usize, 4), dead.len);    try std.testing.expectEqualStrings("repl-gone", dead[0].rule);    try std.testing.expectEqualStrings("no nodes match source 'src.repl.*'", dead[0].reason);    try std.testing.expectEqualStrings("target-gone", dead[1].rule);    try std.testing.expectEqualStrings("no nodes match target 'vendor.*'", dead[1].reason);    try std.testing.expectEqualStrings("repl-stays-gone", dead[2].rule);    try std.testing.expectEqualStrings("no nodes match source 'src.repl.*'", dead[2].reason);    try std.testing.expectEqualStrings("ghost-budget", dead[3].rule);    try std.testing.expectEqualStrings("no nodes match selector 'src.repl.*'", dead[3].reason);}

Complete call list for rules.Check.fileDirectoryCollisions

9 direct calls.

Complete call list for rules.checkDeny

9 direct calls.

Complete caller list for rules.checkNamespaceHandleImports

11 direct callers.

Complete call list for rules.checkNamespaceHandleImports

10 direct calls.

Audit

Definitions34
Public names34
Members31
Version26.7.0
Revisiondaab053ee433