tiny.smg.concepts
Defined in tiny.smg.
API (19)
Actions
Public operations.
addaddSyncPointanalyzecreatehasSyncPointloadloadFromReaderloadSnapshotpublishremoverenderwriteAnalysisJson
Types and contracts
Public types and contracts.
Source
Source: tools/smg/src/concepts.zig
zig
const std = @import("std");const pretty = @import("pretty");const sql = @import("sql");const graph_mod = @import("graph.zig");const limits_mod = @import("limits/root.zig");const search = @import("search.zig");const view = @import("view.zig");const model = @import("model.zig");const storage = @import("storage/root.zig");const pretty_json = pretty.json;const testing_limits = @import("root.zig").default_limits;pub const Concept = struct { name: []const u8, prefixes: []const []const u8 = &.{}, sync_points: []const []const u8 = &.{},};pub const Snapshot = struct { concepts: []const Concept, head: sql.Hash,};pub const Summary = struct { name: []const u8, prefixes: []const []const u8, anchors: []const []const u8, members: usize, internal_edges: usize, cross_in: usize, cross_out: usize, sync_fan_out: usize, sync_density: f64, sync_asymmetry: f64,};pub const RelCount = struct { rel: []const u8, count: usize,};pub const Dependency = struct { source: []const u8, target: []const u8, edge_count: usize, rels: []const RelCount, unsanctioned_count: usize, unsanctioned_rels: []const RelCount, witnesses: []const model.Edge, allowed_sync: bool,};pub const Violation = struct { source: []const u8, target: []const u8, message: []const u8, rels: []const RelCount, source_candidates: []const []const u8, target_candidates: []const []const u8, witnesses: []const model.Edge,};pub const Analysis = struct { declared: []const Summary, dependencies: []const Dependency, violations: []const Violation,};const Materialized = struct { concept: Concept, anchors: []const []const u8, members: []const []const u8,};const Owner = struct { member: []const u8, concept: []const u8,};const Counter = struct { name: []const u8, count: usize,};const PairEdges = struct { source: []const u8, target: []const u8, edges: std.ArrayList(model.Edge) = .empty, unsanctioned: std.ArrayList(model.Edge) = .empty,};const MaterializeResult = struct { materialized: []const Materialized, owners: []const Owner,};pub fn analyze(allocator: std.mem.Allocator, graph: graph_mod.Graph, concept_list: []const Concept, overlap_message: ?*[]const u8) !Analysis { const materialized_result = try materialize(allocator, graph, concept_list, overlap_message); const materialized = materialized_result.materialized; const owners = materialized_result.owners; var internal_edges: std.ArrayList(Counter) = .empty; var cross_in: std.ArrayList(Counter) = .empty; var cross_out: std.ArrayList(Counter) = .empty; var touched_edges: std.ArrayList(Counter) = .empty; var pairs: std.ArrayList(PairEdges) = .empty; for (try view.allEdges(graph, allocator)) |edge| { if (std.mem.eql(u8, edge.rel, model.RelType.contains)) continue; const source_owner = ownerFor(owners, edge.source); const target_owner = ownerFor(owners, edge.target); if (source_owner) |owner| try incrementCounter(allocator, &touched_edges, owner); if (target_owner) |owner| { if (source_owner == null or !std.mem.eql(u8, source_owner.?, owner)) try incrementCounter(allocator, &touched_edges, owner); } if (source_owner == null or target_owner == null) continue; if (std.mem.eql(u8, source_owner.?, target_owner.?)) { try incrementCounter(allocator, &internal_edges, source_owner.?); continue; } try incrementCounter(allocator, &cross_out, source_owner.?); try incrementCounter(allocator, &cross_in, target_owner.?); const pair_index = try pairIndex(allocator, &pairs, source_owner.?, target_owner.?); try pairs.items[pair_index].edges.append(allocator, edge); const source_concept = conceptByName(materialized, source_owner.?).?; const target_concept = conceptByName(materialized, target_owner.?).?; if (!allowedSync(edge, source_concept.concept, target_concept.concept)) try pairs.items[pair_index].unsanctioned.append(allocator, edge); } std.mem.sort(PairEdges, pairs.items, {}, pairLess); var declared: std.ArrayList(Summary) = .empty; for (materialized) |item| { const in_count = counterValue(cross_in.items, item.concept.name); const out_count = counterValue(cross_out.items, item.concept.name); const touched = counterValue(touched_edges.items, item.concept.name); try declared.append(allocator, .{ .name = item.concept.name, .prefixes = item.concept.prefixes, .anchors = item.anchors, .members = item.members.len, .internal_edges = counterValue(internal_edges.items, item.concept.name), .cross_in = in_count, .cross_out = out_count, .sync_fan_out = try syncFanOut(allocator, pairs.items, item.concept.name), .sync_density = if (touched == 0) 0.0 else roundFloat(@as(f64, @floatFromInt(in_count + out_count)) / @as(f64, @floatFromInt(touched)), 6), .sync_asymmetry = roundFloat(try syncAsymmetry(allocator, pairs.items, item.concept.name), 6), }); } var dependencies: std.ArrayList(Dependency) = .empty; var violations: std.ArrayList(Violation) = .empty; for (pairs.items) |pair| { const edges = try edgeSlice(allocator, pair.edges.items); const unsanctioned = try edgeSlice(allocator, pair.unsanctioned.items); const dependency_witnesses = if (unsanctioned.len != 0) try witnessEdges(allocator, unsanctioned) else try witnessEdges(allocator, edges); try dependencies.append(allocator, .{ .source = pair.source, .target = pair.target, .edge_count = edges.len, .rels = try relCounts(allocator, edges), .unsanctioned_count = unsanctioned.len, .unsanctioned_rels = try relCounts(allocator, unsanctioned), .witnesses = dependency_witnesses, .allowed_sync = unsanctioned.len == 0, }); if (unsanctioned.len != 0) { try violations.append(allocator, .{ .source = pair.source, .target = pair.target, .message = try std.fmt.allocPrint(allocator, "{d} unsanctioned cross-concept edge(s)", .{unsanctioned.len}), .rels = try relCounts(allocator, unsanctioned), .source_candidates = try candidateSources(allocator, unsanctioned), .target_candidates = try candidateTargets(allocator, unsanctioned), .witnesses = try witnessEdges(allocator, unsanctioned), }); } } return .{ .declared = try declared.toOwnedSlice(allocator), .dependencies = try dependencies.toOwnedSlice(allocator), .violations = try violations.toOwnedSlice(allocator), };}pub fn writeAnalysisJson(allocator: std.mem.Allocator, writer: *pretty_json.Writer, result: Analysis) !void { try writer.beginObject(); try writer.objectField("declared"); try writer.beginArray(); for (result.declared) |summary| { try writer.beginObject(); try writer.objectField("name"); try writer.write(summary.name); try writer.objectField("prefixes"); try writeStringArray(writer, summary.prefixes); try writer.objectField("anchors"); try writeStringArray(writer, summary.anchors); try writer.objectField("members"); try writer.write(summary.members); try writer.objectField("internal_edges"); try writer.write(summary.internal_edges); try writer.objectField("cross_in"); try writer.write(summary.cross_in); try writer.objectField("cross_out"); try writer.write(summary.cross_out); try writer.objectField("sync_fan_out"); try writer.write(summary.sync_fan_out); try writer.objectField("sync_density"); try writeFloat(allocator, writer, summary.sync_density); try writer.objectField("sync_asymmetry"); try writeFloat(allocator, writer, summary.sync_asymmetry); try writer.endObject(); } try writer.endArray(); try writer.objectField("dependencies"); try writer.beginArray(); for (result.dependencies) |dependency| { try writer.beginObject(); try writer.objectField("source"); try writer.write(dependency.source); try writer.objectField("target"); try writer.write(dependency.target); try writer.objectField("edge_count"); try writer.write(dependency.edge_count); try writer.objectField("rels"); try writeRelCounts(writer, dependency.rels); try writer.objectField("unsanctioned_count"); try writer.write(dependency.unsanctioned_count); try writer.objectField("unsanctioned_rels"); try writeRelCounts(writer, dependency.unsanctioned_rels); try writer.objectField("witnesses"); try writeWitnesses(writer, dependency.witnesses); try writer.objectField("allowed_sync"); try writer.write(dependency.allowed_sync); try writer.endObject(); } try writer.endArray(); try writer.objectField("violations"); try writer.beginArray(); for (result.violations) |violation| { try writer.beginObject(); try writer.objectField("source"); try writer.write(violation.source); try writer.objectField("target"); try writer.write(violation.target); try writer.objectField("message"); try writer.write(violation.message); try writer.objectField("rels"); try writeRelCounts(writer, violation.rels); try writer.objectField("sync_candidates"); try writer.beginObject(); try writer.objectField("source"); try writeStringArray(writer, violation.source_candidates); try writer.objectField("target"); try writeStringArray(writer, violation.target_candidates); try writer.endObject(); try writer.objectField("sync_commands"); try writer.beginObject(); try writer.objectField("source"); try writer.beginArray(); for (violation.source_candidates) |candidate| { try writer.write(try std.fmt.allocPrint(allocator, "smg concept sync-point {s} {s}", .{ violation.source, candidate })); } try writer.endArray(); try writer.objectField("target"); try writer.beginArray(); for (violation.target_candidates) |candidate| { try writer.write(try std.fmt.allocPrint(allocator, "smg concept sync-point {s} {s}", .{ violation.target, candidate })); } try writer.endArray(); try writer.endObject(); try writer.objectField("witnesses"); try writeWitnesses(writer, violation.witnesses); try writer.endObject(); } try writer.endArray(); try writer.endObject();}fn materialize(allocator: std.mem.Allocator, graph: graph_mod.Graph, concept_list: []const Concept, overlap_message: ?*[]const u8) !MaterializeResult { const sorted = try allocator.dupe(Concept, concept_list); std.mem.sort(Concept, sorted, {}, conceptLess); var package_or_module: std.ArrayList([]const u8) = .empty; var all_names: std.ArrayList([]const u8) = .empty; for (try view.allNodes(graph, allocator, null)) |node| { try all_names.append(allocator, node.name); if (std.mem.eql(u8, node.type, model.NodeType.package) or std.mem.eql(u8, node.type, model.NodeType.module)) try package_or_module.append(allocator, node.name); } var out: std.ArrayList(Materialized) = .empty; var owners: std.ArrayList(Owner) = .empty; for (sorted) |concept| { const anchors = try resolveAnchors(allocator, package_or_module.items, concept.prefixes); var members: std.ArrayList([]const u8) = .empty; for (all_names.items) |name| { for (anchors) |anchor| { if (matchesPrefix(name, anchor)) { try appendUniqueList(allocator, &members, name); break; } } } std.mem.sort([]const u8, members.items, {}, stringLess); for (members.items) |member| { if (ownerFor(owners.items, member)) |owner| { if (!std.mem.eql(u8, owner, concept.name)) { if (overlap_message) |message| message.* = try std.fmt.allocPrint(allocator, "concept overlap on '{s}': '{s}' and '{s}' both claim it", .{ member, owner, concept.name }); return error.ConceptOverlap; } } try owners.append(allocator, .{ .member = member, .concept = concept.name }); } try out.append(allocator, .{ .concept = concept, .anchors = anchors, .members = try members.toOwnedSlice(allocator) }); } return .{ .materialized = try out.toOwnedSlice(allocator), .owners = try owners.toOwnedSlice(allocator) };}fn resolveAnchors(allocator: std.mem.Allocator, package_or_module: []const []const u8, prefixes: []const []const u8) ![]const []const u8 { var matches: std.ArrayList([]const u8) = .empty; for (prefixes) |prefix| { for (package_or_module) |name| { if (matchesPrefix(name, prefix)) try appendUniqueList(allocator, &matches, name); } } std.mem.sort([]const u8, matches.items, {}, anchorLess); var anchors: std.ArrayList([]const u8) = .empty; for (matches.items) |name| { var covered = false; for (anchors.items) |anchor| { if (matchesPrefix(name, anchor)) { covered = true; break; } } if (!covered) try anchors.append(allocator, name); } return try anchors.toOwnedSlice(allocator);}fn conceptByName(materialized: []const Materialized, name: []const u8) ?Materialized { for (materialized) |item| if (std.mem.eql(u8, item.concept.name, name)) return item; return null;}fn ownerFor(owners: []const Owner, member: []const u8) ?[]const u8 { for (owners) |owner| if (std.mem.eql(u8, owner.member, member)) return owner.concept; return null;}fn incrementCounter(allocator: std.mem.Allocator, counters: *std.ArrayList(Counter), name: []const u8) !void { for (counters.items) |*counter| { if (std.mem.eql(u8, counter.name, name)) { counter.count += 1; return; } } try counters.append(allocator, .{ .name = name, .count = 1 });}fn counterValue(counters: []const Counter, name: []const u8) usize { for (counters) |counter| if (std.mem.eql(u8, counter.name, name)) return counter.count; return 0;}fn pairIndex(allocator: std.mem.Allocator, pairs: *std.ArrayList(PairEdges), source: []const u8, target: []const u8) !usize { for (pairs.items, 0..) |pair, index| { if (std.mem.eql(u8, pair.source, source) and std.mem.eql(u8, pair.target, target)) return index; } try pairs.append(allocator, .{ .source = source, .target = target }); return pairs.items.len - 1;}fn allowedSync(edge: model.Edge, source: Concept, target: Concept) bool { return matchesAnyPrefix(edge.source, source.sync_points) or matchesAnyPrefix(edge.target, target.sync_points);}fn matchesAnyPrefix(name: []const u8, prefixes: []const []const u8) bool { for (prefixes) |prefix| if (matchesPrefix(name, prefix)) return true; return false;}fn syncFanOut(allocator: std.mem.Allocator, pairs: []const PairEdges, name: []const u8) !usize { var targets: std.ArrayList([]const u8) = .empty; for (pairs) |pair| { if (std.mem.eql(u8, pair.source, name)) try appendUniqueList(allocator, &targets, pair.target); } return targets.items.len;}fn syncAsymmetry(allocator: std.mem.Allocator, pairs: []const PairEdges, name: []const u8) !f64 { var related: std.ArrayList([]const u8) = .empty; for (pairs) |pair| { if (std.mem.eql(u8, pair.source, name)) try appendUniqueList(allocator, &related, pair.target); if (std.mem.eql(u8, pair.target, name)) try appendUniqueList(allocator, &related, pair.source); } if (related.items.len == 0) return 0.0; var bidirectional: usize = 0; var unidirectional: usize = 0; for (related.items) |other| { const outgoing = hasPair(pairs, name, other); const incoming = hasPair(pairs, other, name); if (outgoing and incoming) { bidirectional += 1; } else { unidirectional += 1; } } return @as(f64, @floatFromInt(unidirectional)) / @as(f64, @floatFromInt(@max(@as(usize, 1), bidirectional)));}fn hasPair(pairs: []const PairEdges, source: []const u8, target: []const u8) bool { for (pairs) |pair| if (std.mem.eql(u8, pair.source, source) and std.mem.eql(u8, pair.target, target)) return true; return false;}fn edgeSlice(allocator: std.mem.Allocator, edges: []const model.Edge) ![]const model.Edge { const out = try allocator.dupe(model.Edge, edges); std.mem.sort(model.Edge, out, {}, edgeLess); return out;}fn witnessEdges(allocator: std.mem.Allocator, edges: []const model.Edge) ![]const model.Edge { const count = @min(edges.len, 5); return try allocator.dupe(model.Edge, edges[0..count]);}fn relCounts(allocator: std.mem.Allocator, edges: []const model.Edge) ![]const RelCount { var out: std.ArrayList(RelCount) = .empty; for (edges) |edge| { var found = false; for (out.items) |*item| { if (std.mem.eql(u8, item.rel, edge.rel)) { item.count += 1; found = true; break; } } if (!found) try out.append(allocator, .{ .rel = edge.rel, .count = 1 }); } std.mem.sort(RelCount, out.items, {}, relCountLess); return try out.toOwnedSlice(allocator);}fn candidateSources(allocator: std.mem.Allocator, edges: []const model.Edge) ![]const []const u8 { var out: std.ArrayList([]const u8) = .empty; for (edges) |edge| try appendUniqueList(allocator, &out, edge.source); return try limitStrings(allocator, out.items, 5);}fn candidateTargets(allocator: std.mem.Allocator, edges: []const model.Edge) ![]const []const u8 { var out: std.ArrayList([]const u8) = .empty; for (edges) |edge| try appendUniqueList(allocator, &out, edge.target); return try limitStrings(allocator, out.items, 5);}fn limitStrings(allocator: std.mem.Allocator, values: []const []const u8, limit: usize) ![]const []const u8 { return try allocator.dupe([]const u8, values[0..@min(values.len, limit)]);}fn writeStringArray(writer: *pretty_json.Writer, values: []const []const u8) !void { try writer.beginArray(); for (values) |value| try writer.write(value); try writer.endArray();}fn writeRelCounts(writer: *pretty_json.Writer, counts: []const RelCount) !void { try writer.beginObject(); for (counts) |count| { try writer.objectField(count.rel); try writer.write(count.count); } try writer.endObject();}fn writeWitnesses(writer: *pretty_json.Writer, edges: []const model.Edge) !void { try writer.beginArray(); for (edges) |edge| { try writer.beginObject(); try writer.objectField("kind"); try writer.write("edge"); try writer.objectField("edges"); try writer.beginArray(); try writeEdgeBrief(writer, edge); try writer.endArray(); try writer.endObject(); } try writer.endArray();}fn writeEdgeBrief(writer: *pretty_json.Writer, edge: model.Edge) !void { try writer.beginObject(); try writer.objectField("source"); try writer.write(edge.source); try writer.objectField("rel"); try writer.write(edge.rel); try writer.objectField("target"); try writer.write(edge.target); try writer.endObject();}fn writeFloat(allocator: std.mem.Allocator, writer: *pretty_json.Writer, value: f64) !void { const raw = try std.fmt.allocPrint(allocator, "{d:.6}", .{value}); var end = raw.len; while (end > 0 and raw[end - 1] == '0') end -= 1; if (end > 0 and raw[end - 1] == '.') end += 1; try writer.raw(raw[0..end]);}fn roundFloat(value: f64, decimals: usize) f64 { var scale: f64 = 1.0; for (0..decimals) |_| scale *= 10.0; return @round(value * scale) / scale;}fn matchesPrefix(name: []const u8, prefix: []const u8) bool { return std.mem.eql(u8, name, prefix) or (name.len > prefix.len and std.mem.startsWith(u8, name, prefix) and name[prefix.len] == '.');}fn countDots(name: []const u8) usize { var count: usize = 0; for (name) |byte| { if (byte == '.') count += 1; } return count;}fn conceptLess(_: void, a: Concept, b: Concept) bool { return std.mem.lessThan(u8, a.name, b.name);}fn anchorLess(_: void, a: []const u8, b: []const u8) bool { const dots_a = countDots(a); const dots_b = countDots(b); if (dots_a != dots_b) return dots_a < dots_b; return std.mem.lessThan(u8, a, b);}fn pairLess(_: void, a: PairEdges, b: PairEdges) bool { const source = std.mem.order(u8, a.source, b.source); if (source != .eq) return source == .lt; return std.mem.lessThan(u8, a.target, b.target);}fn edgeLess(_: void, a: model.Edge, b: model.Edge) bool { const source = std.mem.order(u8, a.source, b.source); if (source != .eq) return source == .lt; const rel = std.mem.order(u8, a.rel, b.rel); if (rel != .eq) return rel == .lt; return std.mem.lessThan(u8, a.target, b.target);}fn relCountLess(_: void, a: RelCount, b: RelCount) bool { return std.mem.lessThan(u8, a.rel, b.rel);}fn stringLess(_: void, a: []const u8, b: []const u8) bool { return std.mem.lessThan(u8, a, b);}pub fn load( allocator: std.mem.Allocator, root: []const u8, limits: limits_mod.Storage,) ![]const Concept { return (try loadSnapshot(allocator, root, limits)).concepts;}pub fn loadSnapshot( allocator: std.mem.Allocator, root: []const u8, limits: limits_mod.Storage,) !Snapshot { var opened = try storage.store.openRead(allocator, root, limits); defer opened.close(); return .{ .concepts = try loadFromReader(allocator, &opened.reader), .head = opened.reader.head, };}pub fn loadFromReader(allocator: std.mem.Allocator, reader: *storage.database.Reader) ![]const Concept { const documents = try storage.database.read.readerConceptDocuments(reader, allocator, allocator); defer storage.rows.freeConceptDocumentRowSlice(allocator, allocator, documents); var out: std.ArrayList(Concept) = .empty; for (documents) |document| { var parsed = std.json.parseFromSlice(std.json.Value, allocator, document.document, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => continue, }; defer parsed.deinit(); const object = switch (parsed.value) { .object => |object| object, else => continue, }; const name = stringField(object, "name") orelse continue; try out.append(allocator, .{ .name = try allocator.dupe(u8, name), .prefixes = try stringArray(allocator, object.get("prefixes")), .sync_points = try stringArray(allocator, object.get("sync_points")), }); } return try out.toOwnedSlice(allocator);}pub fn publish( allocator: std.mem.Allocator, root: []const u8, concepts: []const Concept, expected_head: sql.Hash, limits: limits_mod.Limits,) !void { const sorted = try allocator.dupe(Concept, concepts); defer allocator.free(sorted); std.mem.sort(Concept, sorted, {}, conceptLess); const puts = try allocator.alloc(storage.rows.ConceptDocumentPut, sorted.len); defer allocator.free(puts); var rendered: std.ArrayList([]const u8) = .empty; defer { for (rendered.items) |document| allocator.free(document); rendered.deinit(allocator); } try rendered.ensureTotalCapacityPrecise(allocator, sorted.len); for (sorted, 0..) |concept, index| { const document = try render(allocator, concept); rendered.appendAssumeCapacity(document); puts[index] = .{ .rowid = @intCast(index + 1), .name = concept.name, .document = document, }; } var publisher = try storage.store.beginPublish( allocator, root, expected_head, limits.storage, ); defer publisher.close(); const existing = try storage.database.read.databaseConceptDocuments(&publisher.store.database, allocator, allocator); defer storage.rows.freeConceptDocumentRowSlice(allocator, allocator, existing); const deletes = try allocator.alloc(i64, existing.len); defer allocator.free(deletes); for (existing, 0..) |document, index| deletes[index] = document.rowid; try storage.database.edits.apply(&publisher.store.database, .{ .concept_document_puts = puts, .concept_document_deletes = deletes, }); try storage.database.commitIfDirty(&publisher.store.database); const head = (try publisher.store.database.connection.checkout()).head; try storage.summary.writeForHead(allocator, root, &publisher.store.database, head); _ = try search.syncStoredFromDatabase( allocator, root, &publisher.store.database, expected_head, head, &.{}, &.{}, limits.search, ); try publisher.finish();}pub fn add(allocator: std.mem.Allocator, concepts: []const Concept, name: []const u8, prefix: []const u8) ![]const Concept { var out: std.ArrayList(Concept) = .empty; var found = false; for (concepts) |concept| { if (std.mem.eql(u8, concept.name, name)) { found = true; try out.append(allocator, .{ .name = concept.name, .prefixes = try appendUnique(allocator, concept.prefixes, prefix), .sync_points = concept.sync_points, }); } else { try out.append(allocator, concept); } } if (!found) { const prefixes = try allocator.alloc([]const u8, 1); prefixes[0] = try allocator.dupe(u8, prefix); try out.append(allocator, .{ .name = try allocator.dupe(u8, name), .prefixes = prefixes }); } return try out.toOwnedSlice(allocator);}pub fn create(allocator: std.mem.Allocator, concepts: []const Concept, name: []const u8, prefixes: []const []const u8, sync_points: []const []const u8) ![]const Concept { for (concepts) |concept| if (std.mem.eql(u8, concept.name, name)) return error.DuplicateConcept; var out: std.ArrayList(Concept) = .empty; for (concepts) |concept| try out.append(allocator, concept); const owned_prefixes = try allocator.alloc([]const u8, prefixes.len); for (prefixes, 0..) |prefix, index| owned_prefixes[index] = try allocator.dupe(u8, prefix); const owned_points = try allocator.alloc([]const u8, sync_points.len); for (sync_points, 0..) |point, index| owned_points[index] = try allocator.dupe(u8, point); try out.append(allocator, .{ .name = try allocator.dupe(u8, name), .prefixes = owned_prefixes, .sync_points = owned_points }); return try out.toOwnedSlice(allocator);}pub fn hasSyncPoint(concepts: []const Concept, name: []const u8, point: []const u8) bool { for (concepts) |concept| { if (!std.mem.eql(u8, concept.name, name)) continue; for (concept.sync_points) |existing| if (std.mem.eql(u8, existing, point)) return true; return false; } return false;}pub fn addSyncPoint(allocator: std.mem.Allocator, concepts: []const Concept, name: []const u8, point: []const u8) ![]const Concept { var out: std.ArrayList(Concept) = .empty; var found = false; for (concepts) |concept| { if (std.mem.eql(u8, concept.name, name)) { found = true; try out.append(allocator, .{ .name = concept.name, .prefixes = concept.prefixes, .sync_points = try appendUnique(allocator, concept.sync_points, point), }); } else { try out.append(allocator, concept); } } if (!found) return error.NotFound; return try out.toOwnedSlice(allocator);}pub fn remove(allocator: std.mem.Allocator, concepts: []const Concept, name: []const u8) ![]const Concept { var out: std.ArrayList(Concept) = .empty; var found = false; for (concepts) |concept| { if (std.mem.eql(u8, concept.name, name)) { found = true; } else { try out.append(allocator, concept); } } if (!found) return error.NotFound; return try out.toOwnedSlice(allocator);}pub fn render(allocator: std.mem.Allocator, concept: Concept) ![]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("concept"); try writer.objectField("name"); try writer.write(concept.name); try writer.objectField("prefixes"); try writeStringArray(&writer, concept.prefixes); if (concept.sync_points.len != 0) { try writer.objectField("sync_points"); try writeStringArray(&writer, concept.sync_points); } try writer.endObject(); return try out.toOwnedSlice();}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 stringArray(allocator: std.mem.Allocator, maybe: ?std.json.Value) ![]const []const u8 { const value = maybe orelse return &.{}; const array = switch (value) { .array => |array| array, else => return &.{}, }; var out: std.ArrayList([]const u8) = .empty; for (array.items) |item| { if (item == .string) try out.append(allocator, try allocator.dupe(u8, item.string)); } return try out.toOwnedSlice(allocator);}test "concept declarations persist through the smg SQL 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); _ = try storage.initProject(allocator, root, testing_limits.storage); const values = [_]Concept{ .{ .name = "runtime", .prefixes = &.{ "app.runtime", "lib.runtime" }, .sync_points = &.{"app.runtime.api"} }, .{ .name = "cli", .prefixes = &.{"app.cli"} }, }; const snapshot = try loadSnapshot(allocator, root, testing_limits.storage); try publish(allocator, root, &values, snapshot.head, testing_limits); const loaded = try load(allocator, root, testing_limits.storage); try std.testing.expectEqual(@as(usize, 2), loaded.len); try std.testing.expectEqualStrings("cli", loaded[0].name); try std.testing.expectEqualStrings("runtime", loaded[1].name); try std.testing.expectEqual(@as(usize, 2), loaded[1].prefixes.len); try std.testing.expectEqualStrings("app.runtime.api", loaded[1].sync_points[0]);}test "stale concept publication preserves the winner and supports reapply" { 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); _ = try storage.initProject(allocator, root, testing_limits.storage); const source = try loadSnapshot(allocator, root, testing_limits.storage); const winner = try create(allocator, source.concepts, "winner", &.{"app.winner"}, &.{}); const loser = try create(allocator, source.concepts, "loser", &.{"app.loser"}, &.{}); try publish(allocator, root, winner, source.head, testing_limits); const winner_snapshot = try loadSnapshot(allocator, root, testing_limits.storage); try std.testing.expectError(error.StaleSourceHead, publish(allocator, root, loser, source.head, testing_limits)); try std.testing.expect(!try storage.sync.refreshIncomplete(allocator, root)); const after_conflict = try loadSnapshot(allocator, root, testing_limits.storage); try std.testing.expect(sql.version.same(winner_snapshot.head, after_conflict.head)); try std.testing.expectEqual(@as(usize, 1), after_conflict.concepts.len); try std.testing.expectEqualStrings("winner", after_conflict.concepts[0].name); _ = try storage.summary.load(allocator, root, testing_limits.storage); _ = try search.searchStored(allocator, root, "winner", null, 10, testing_limits.storage); const reapplied = try create(allocator, after_conflict.concepts, "loser", &.{"app.loser"}, &.{}); try publish(allocator, root, reapplied, after_conflict.head, testing_limits); const final = try load(allocator, root, testing_limits.storage); try std.testing.expectEqual(@as(usize, 2), final.len); try std.testing.expectEqualStrings("loser", final[0].name); try std.testing.expectEqualStrings("winner", final[1].name);}fn appendUniqueList(allocator: std.mem.Allocator, list: *std.ArrayList([]const u8), value: []const u8) !void { for (list.items) |item| if (std.mem.eql(u8, item, value)) return; try list.append(allocator, value);}fn appendUnique(allocator: std.mem.Allocator, values: []const []const u8, value: []const u8) ![]const []const u8 { for (values) |item| if (std.mem.eql(u8, item, value)) return values; var out: std.ArrayList([]const u8) = .empty; for (values) |item| try out.append(allocator, item); try out.append(allocator, try allocator.dupe(u8, value)); return try out.toOwnedSlice(allocator);}test "analyze concepts matches python json contract" { 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 = "app", .type = model.NodeType.package }); try graph_mod.addNode(&graph, .{ .name = "app.core", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "app.core.Engine", .type = model.NodeType.class }); try graph_mod.addNode(&graph, .{ .name = "app.core.Engine.run", .type = model.NodeType.method }); try graph_mod.addNode(&graph, .{ .name = "app.utils", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "app.utils.helper", .type = model.NodeType.function }); try graph_mod.addEdge(&graph, .{ .source = "app", .rel = model.RelType.contains, .target = "app.core" }); try graph_mod.addEdge(&graph, .{ .source = "app", .rel = model.RelType.contains, .target = "app.utils" }); try graph_mod.addEdge(&graph, .{ .source = "app.core", .rel = model.RelType.contains, .target = "app.core.Engine" }); try graph_mod.addEdge(&graph, .{ .source = "app.core.Engine", .rel = model.RelType.contains, .target = "app.core.Engine.run" }); try graph_mod.addEdge(&graph, .{ .source = "app.utils", .rel = model.RelType.contains, .target = "app.utils.helper" }); try graph_mod.addEdge(&graph, .{ .source = "app.core", .rel = model.RelType.imports, .target = "app.utils" }); try graph_mod.addEdge(&graph, .{ .source = "app.core.Engine.run", .rel = model.RelType.calls, .target = "app.utils.helper" }); const prefix_core = [_][]const u8{"app.core"}; const prefix_utils = [_][]const u8{"app.utils"}; const concept_list = [_]Concept{ .{ .name = "core", .prefixes = &prefix_core }, .{ .name = "utils", .prefixes = &prefix_utils }, }; const result = try analyze(allocator, graph, &concept_list, null); var out: std.Io.Writer.Allocating = .init(allocator); var writer = pretty_json.Writer.init(&out.writer, .minified); try writeAnalysisJson(allocator, &writer, result); try out.writer.writeByte('\n'); try std.testing.expectEqualStrings( \\{"declared":[{"name":"core","prefixes":["app.core"],"anchors":["app.core"],"members":3,"internal_edges":0,"cross_in":0,"cross_out":2,"sync_fan_out":1,"sync_density":1.0,"sync_asymmetry":1.0},{"name":"utils","prefixes":["app.utils"],"anchors":["app.utils"],"members":2,"internal_edges":0,"cross_in":2,"cross_out":0,"sync_fan_out":0,"sync_density":1.0,"sync_asymmetry":1.0}],"dependencies":[{"source":"core","target":"utils","edge_count":2,"rels":{"calls":1,"imports":1},"unsanctioned_count":2,"unsanctioned_rels":{"calls":1,"imports":1},"witnesses":[{"kind":"edge","edges":[{"source":"app.core","rel":"imports","target":"app.utils"}]},{"kind":"edge","edges":[{"source":"app.core.Engine.run","rel":"calls","target":"app.utils.helper"}]}],"allowed_sync":false}],"violations":[{"source":"core","target":"utils","message":"2 unsanctioned cross-concept edge(s)","rels":{"calls":1,"imports":1},"sync_candidates":{"source":["app.core","app.core.Engine.run"],"target":["app.utils","app.utils.helper"]},"sync_commands":{"source":["smg concept sync-point core app.core","smg concept sync-point core app.core.Engine.run"],"target":["smg concept sync-point utils app.utils","smg concept sync-point utils app.utils.helper"]},"witnesses":[{"kind":"edge","edges":[{"source":"app.core","rel":"imports","target":"app.utils"}]},{"kind":"edge","edges":[{"source":"app.core.Engine.run","rel":"calls","target":"app.utils.helper"}]}]}]} \\ , out.written());}test "concept json omits empty sync points like python schema" { const rendered = try render(std.testing.allocator, .{ .name = "utils", .prefixes = &.{"app.utils"} }); defer std.testing.allocator.free(rendered); try std.testing.expectEqualStrings("{\"kind\":\"concept\",\"name\":\"utils\",\"prefixes\":[\"app.utils\"]}", rendered);}test "concept publication sorts by name like python storage" { 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); _ = try storage.initProject(allocator, root, testing_limits.storage); const snapshot = try loadSnapshot(allocator, root, testing_limits.storage); const list = [_]Concept{ .{ .name = "ui", .prefixes = &.{"app.ui"} }, .{ .name = "data", .prefixes = &.{"app.db"} }, }; try publish(allocator, root, &list, snapshot.head, testing_limits); const loaded = try load(allocator, root, testing_limits.storage); try std.testing.expectEqual(@as(usize, 2), loaded.len); try std.testing.expectEqualStrings("data", loaded[0].name); try std.testing.expectEqualStrings("ui", loaded[1].name); try std.testing.expectEqual(@as(usize, 0), (try storage.summary.load(allocator, root, testing_limits.storage)).nodes); try std.testing.expectEqual(@as(i64, 0), (try search.searchStored(allocator, root, "data", null, 10, testing_limits.storage)).total);}Source: tools/smg/src/root.zig:18
zig
pub const concepts = @import("concepts.zig");Complete call list for concepts.analyze
17 direct calls.
lib.pretty.core.src.position.Summary.append[function] — private source atlib/pretty/core/src/position.zig:38in nearest public ownerlib.pretty.core.src.positiontools.smg.src.concepts.allowedSync[function] — private; no exact target attools/smg/src/concepts.zig:367in nearest public ownertiny.smg.conceptstools.smg.src.concepts.candidateSources[function] — private; no exact target attools/smg/src/concepts.zig:438in nearest public ownertiny.smg.conceptstools.smg.src.concepts.candidateTargets[function] — private; no exact target attools/smg/src/concepts.zig:444in nearest public ownertiny.smg.conceptstools.smg.src.concepts.conceptByName[function] — private; no exact target attools/smg/src/concepts.zig:334in nearest public ownertiny.smg.conceptstools.smg.src.concepts.counterValue[function] — private; no exact target attools/smg/src/concepts.zig:354in nearest public ownertiny.smg.conceptstools.smg.src.concepts.edgeSlice[function] — private; no exact target attools/smg/src/concepts.zig:410in nearest public ownertiny.smg.conceptstools.smg.src.concepts.incrementCounter[function] — private; no exact target attools/smg/src/concepts.zig:344in nearest public ownertiny.smg.conceptstools.smg.src.concepts.materialize[function] — private; no exact target attools/smg/src/concepts.zig:275in nearest public ownertiny.smg.conceptstools.smg.src.concepts.ownerFor[function] — private; no exact target attools/smg/src/concepts.zig:339in nearest public ownertiny.smg.conceptstools.smg.src.concepts.pairIndex[function] — private; no exact target attools/smg/src/concepts.zig:359in nearest public ownertiny.smg.conceptstools.smg.src.concepts.relCounts[function] — private; no exact target attools/smg/src/concepts.zig:421in nearest public ownertiny.smg.conceptstools.smg.src.concepts.roundFloat[function] — private; no exact target attools/smg/src/concepts.zig:503in nearest public ownertiny.smg.conceptstools.smg.src.concepts.syncAsymmetry[function] — private; no exact target attools/smg/src/concepts.zig:384in nearest public ownertiny.smg.conceptstools.smg.src.concepts.syncFanOut[function] — private; no exact target attools/smg/src/concepts.zig:376in nearest public ownertiny.smg.conceptstools.smg.src.concepts.witnessEdges[function] — private; no exact target attools/smg/src/concepts.zig:416in nearest public ownertiny.smg.conceptstiny.smg.view.allEdges[function] attools/smg/src/view.zig:245
Complete caller list for concepts.loadSnapshot
7 direct callers.
tools.smg.src.command.concepts.command.add[function] — private; no exact target attools/smg/src/command/concepts/command.zig:48in nearest public ownertiny.smg.command.concepts.commandtools.smg.src.command.concepts.command.remove[function] — private; no exact target attools/smg/src/command/concepts/command.zig:80in nearest public ownertiny.smg.command.concepts.commandtools.smg.src.command.concepts.command.syncPoint[function] — private; no exact target attools/smg/src/command/concepts/command.zig:105in nearest public ownertiny.smg.command.concepts.commandtiny.smg.concepts.load[function] attools/smg/src/concepts.zig:554tools.smg.src.concepts.test_concept_declarations_persist_through_the_smg_SQL_store[function] — test; no exact target attools/smg/src/concepts.zig:775in nearest public ownertiny.smg.conceptstools.smg.src.concepts.test_concept_publication_sorts_by_name_like_python_storage[function] — test; no exact target attools/smg/src/concepts.zig:884in nearest public ownertiny.smg.conceptstools.smg.src.concepts.test_stale_concept_publication_preserves_the_winner_and_supports_reapply[function] — test; no exact target attools/smg/src/concepts.zig:798in nearest public ownertiny.smg.concepts
Audit
| Definitions | 20 |
|---|---|
| Public names | 20 |
| Members | 35 |
| Version | 26.7.0 |
| Revision | daab053ee433 |