tiny.smg.rules
Defined in tiny.smg.
API (35)
Actions
Public operations.
Check.fileDirectoryCollisionsCheck.invariantCheck.prepareCheck.ruleaddaddStoredassertionErrorMessagecheckcheckDenycheckErrorMessagecheckFileDirectoryCollisionscheckInvariantcheckNamespaceHandleImportscheckRuledeadRulesloadparseDenyPatternquantifiedMetricValidationMessageremoveremoveStoredrendersetNamespaceHandleImportBaselinesetNamespaceHandleImportBaselineStoredunknownAssertionMetricsvalidateAssertionvalidateNamespaceHandleImportBaseline
Types and contracts
Public types and contracts.
CheckDeadRuleEdgeFindingPlanFactValueFindingNodeFindingPlanPredicateFactPredicateWitnessRule
Source
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 = ¶ms, }, .{ .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 = ¶ms, }; 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 = ¶ms, }; 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 = ¶ms, }; 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 = ¶ms, }; 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.
tools.smg.src.rules.Check.parentFilter[method] — private; no exact target attools/smg/src/rules.zig:589in nearest public ownertiny.smg.rulestiny.smg.NodeFindingPlan.activate[method] attools/smg/src/rules.zig:311tiny.smg.NodeFindingPlan.append[method] attools/smg/src/rules.zig:317tiny.smg.NodeFindingPlan.deinit[method] attools/smg/src/rules.zig:340tiny.smg.NodeFindingPlan.init[function] attools/smg/src/rules.zig:302tiny.smg.NodeFindingPlan.sort[method] attools/smg/src/rules.zig:324tiny.smg.NodeFindingPlan.transfer[method] attools/smg/src/rules.zig:330tools.smg.src.rules.fileDirectoryCollision[function] — private; no exact target attools/smg/src/rules.zig:864in nearest public ownertiny.smg.rulestools.smg.src.rules.fileDirectoryCollisionCount[function] — private; no exact target attools/smg/src/rules.zig:851in nearest public ownertiny.smg.rules
Complete call list for rules.checkDeny
9 direct calls.
tiny.smg.EdgeFindingPlan.activate[method] attools/smg/src/rules.zig:155tiny.smg.EdgeFindingPlan.append[method] attools/smg/src/rules.zig:161tiny.smg.EdgeFindingPlan.deinit[method] attools/smg/src/rules.zig:184tiny.smg.EdgeFindingPlan.init[function] attools/smg/src/rules.zig:146tiny.smg.EdgeFindingPlan.sort[method] attools/smg/src/rules.zig:168tiny.smg.EdgeFindingPlan.transfer[method] attools/smg/src/rules.zig:174tools.smg.src.rules.denyEdgeCount[function] — private; no exact target attools/smg/src/rules.zig:730in nearest public ownertiny.smg.rulestools.smg.src.rules.denyEdgeMatches[function] — private; no exact target attools/smg/src/rules.zig:739in nearest public ownertiny.smg.rulestiny.smg.rules.parseDenyPattern[function] attools/smg/src/rules.zig:1270
Complete caller list for rules.checkNamespaceHandleImports
11 direct callers.
tiny.smg.rules.Check.invariant[method] attools/smg/src/rules.zig:547tools.smg.src.rules.test_namespace_handle_import_baseline_detects_exact_edge-set_drift[function] — test; no exact target attools/smg/src/rules.zig:2714in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_import_baseline_identifies_equal-count_replacement[function] — test; no exact target attools/smg/src/rules.zig:2754in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_import_baseline_rejects_stale_debt[function] — test; no exact target attools/smg/src/rules.zig:2798in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_import_baseline_requires_a_complete_digest[function] — test; no exact target attools/smg/src/rules.zig:2824in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_imports_allow_child_test_discovery_only_from_tests[function] — test; no exact target attools/smg/src/rules.zig:2660in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_imports_allow_cross_owner_root_handles[function] — test; no exact target attools/smg/src/rules.zig:2648in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_imports_allow_same_owner_implementations[function] — test; no exact target attools/smg/src/rules.zig:2610in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_imports_deny_parent_child_and_sibling_implementations[function] — test; no exact target attools/smg/src/rules.zig:2624in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_imports_ignore_unresolved_external_nodes[function] — test; no exact target attools/smg/src/rules.zig:2677in nearest public ownertiny.smg.rulestools.smg.src.rules.test_namespace_handle_imports_respect_source_scope[function] — test; no exact target attools/smg/src/rules.zig:2691in nearest public ownertiny.smg.rules
Complete call list for rules.checkNamespaceHandleImports
10 direct calls.
tiny.smg.EdgeFindingPlan.activate[method] attools/smg/src/rules.zig:155tiny.smg.EdgeFindingPlan.append[method] attools/smg/src/rules.zig:161tiny.smg.EdgeFindingPlan.deinit[method] attools/smg/src/rules.zig:184tiny.smg.EdgeFindingPlan.init[function] attools/smg/src/rules.zig:146tiny.smg.EdgeFindingPlan.sort[method] attools/smg/src/rules.zig:168tiny.smg.EdgeFindingPlan.transfer[method] attools/smg/src/rules.zig:174tools.smg.src.rules.namespaceHandleImportBaseline[function] — private; no exact target attools/smg/src/rules.zig:948in nearest public ownertiny.smg.rulestools.smg.src.rules.namespaceHandleImportCount[function] — private; no exact target attools/smg/src/rules.zig:981in nearest public ownertiny.smg.rulestools.smg.src.rules.namespaceHandleImportDigest[function] — private; no exact target attools/smg/src/rules.zig:968in nearest public ownertiny.smg.rulestools.smg.src.rules.namespaceHandleImportViolation[function] — private; no exact target attools/smg/src/rules.zig:990in nearest public ownertiny.smg.rules
Audit
| Definitions | 34 |
|---|---|
| Public names | 34 |
| Members | 31 |
| Version | 26.7.0 |
| Revision | daab053ee433 |