Skip to content

Commit b3e7b58

Browse files
Antigravity Agentclaude
andcommitted
feat(cytoplasm): L2 Cell Search & Discovery -- fuzzy search
- Add `tri cell search <query>` — fuzzy match by name/id/description - Add `tri cell find --capability X` — filter by contributes.commands/exports - Add `tri cell list --tag X:Y` — filter by tags (scope:type) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e511dac commit b3e7b58

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed

src/tri/cytoplasm.zig

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ pub fn runCellCommand(allocator: Allocator, args: []const []const u8) !void {
187187
if (std.mem.eql(u8, sub, "version")) return runVersion(allocator, rest);
188188
if (std.mem.eql(u8, sub, "outdated")) return runOutdated(allocator, rest);
189189
if (std.mem.eql(u8, sub, "regenerate")) return runRegenerate(allocator, rest);
190+
if (std.mem.eql(u8, sub, "search")) return runSearch(allocator, rest);
191+
if (std.mem.eql(u8, sub, "find")) return runFind(allocator, rest);
190192

191193
printHelp();
192194
}
@@ -250,6 +252,9 @@ fn printHelp() void {
250252
std.debug.print(" {s}version{s} Show cell versions and content hashes\n", .{ GREEN, RESET });
251253
std.debug.print(" {s}outdated{s} List cells with modified content (needs regen)\n", .{ GREEN, RESET });
252254
std.debug.print(" {s}regenerate --outdated{s} Regenerate all outdated cells\n", .{ GREEN, RESET });
255+
std.debug.print(" {s}search <query>{s} Fuzzy search by name/id/description\n", .{ GREEN, RESET });
256+
std.debug.print(" {s}find --capability X{s} Find cells with specific capability\n", .{ GREEN, RESET });
257+
std.debug.print(" {s}list --tag X:Y{s} Filter by tags (scope:brain, type:library)\n", .{ GREEN, RESET });
253258
}
254259

255260
// ═══════════════════════════════════════════════════════════════════════════════
@@ -277,6 +282,7 @@ fn runList(allocator: Allocator, args: []const []const u8) !void {
277282
var owner_filter: ?[]const u8 = null;
278283
var scope_filter: ?[]const u8 = null;
279284
var type_filter: ?[]const u8 = null;
285+
var tag_filter: ?[]const u8 = null;
280286
var show_commands = false;
281287
var show_health = false;
282288
var show_group = false;
@@ -291,6 +297,9 @@ fn runList(allocator: Allocator, args: []const []const u8) !void {
291297
} else if (std.mem.eql(u8, args[i], "--type") and i + 1 < args.len) {
292298
type_filter = args[i + 1];
293299
i += 1;
300+
} else if (std.mem.eql(u8, args[i], "--tag") and i + 1 < args.len) {
301+
tag_filter = args[i + 1];
302+
i += 1;
294303
} else if (std.mem.eql(u8, args[i], "--commands")) {
295304
show_commands = true;
296305
} else if (std.mem.eql(u8, args[i], "--health")) {
@@ -300,6 +309,20 @@ fn runList(allocator: Allocator, args: []const []const u8) !void {
300309
}
301310
}
302311

312+
// Process --tag filter (format: "key:value" or "*:value")
313+
if (tag_filter) |tf| {
314+
if (std.mem.indexOf(u8, tf, ":")) |colon_idx| {
315+
const key = tf[0..colon_idx];
316+
const value = tf[colon_idx + 1 ..];
317+
if (std.mem.eql(u8, key, "scope")) {
318+
scope_filter = value;
319+
} else if (std.mem.eql(u8, key, "type")) {
320+
type_filter = value;
321+
}
322+
// Could support more tag keys in future
323+
}
324+
}
325+
303326
std.debug.print("\n{s}🐝 TRINITY HONEYCOMB — {d} cells{s}\n", .{ GOLDEN, items.len, RESET });
304327
std.debug.print("{s}Core version: {s}{s}\n\n", .{ GRAY, CORE_VERSION, RESET });
305328

@@ -474,6 +497,237 @@ fn runList(allocator: Allocator, args: []const []const u8) !void {
474497
std.debug.print(" | Files: {d} | Tests: {d}\n\n", .{ total_files, total_tests });
475498
}
476499

500+
// ═══════════════════════════════════════════════════════════════════════════════
501+
// SEARCH — fuzzy search by name/id/description
502+
// ═══════════════════════════════════════════════════════════════════════════════
503+
504+
fn runSearch(allocator: Allocator, args: []const []const u8) !void {
505+
if (args.len == 0) {
506+
std.debug.print("{s}Usage:{s} tri cell search <query>\n", .{ YELLOW, RESET });
507+
std.debug.print(" Example: tri cell search faculty\n", .{});
508+
std.debug.print(" Searches: cell ID, name, description\n\n", .{});
509+
return;
510+
}
511+
512+
const query = args[0];
513+
const query_lower = try allocator.dupe(u8, query);
514+
defer allocator.free(query_lower);
515+
516+
// Convert to lowercase for case-insensitive search
517+
for (query_lower, 0..) |c, i| {
518+
query_lower[i] = toLower(c);
519+
}
520+
521+
const registry = try loadRegistry(allocator);
522+
defer allocator.free(registry);
523+
524+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, registry, .{}) catch {
525+
std.debug.print("{s}ERROR{s}: Failed to parse registry\n", .{ RED, RESET });
526+
return;
527+
};
528+
defer parsed.deinit();
529+
530+
const cells = (parsed.value.object.get("cells") orelse return).array.items;
531+
532+
std.debug.print("\n{s}🔍 SEARCH: \"{s}\"{s}\n\n", .{ CYAN, query, RESET });
533+
534+
var match_count: usize = 0;
535+
536+
for (cells) |item| {
537+
const obj = item.object;
538+
const id = jsonStr(obj, "id");
539+
const path = jsonStr(obj, "path");
540+
541+
// Load cell.tri for name/description
542+
const cell_tri_path = std.fmt.allocPrint(allocator, "{s}/cell.tri", .{path}) catch continue;
543+
defer allocator.free(cell_tri_path);
544+
545+
const cell_content = std.fs.cwd().readFileAlloc(allocator, cell_tri_path, 65536) catch continue;
546+
defer allocator.free(cell_content);
547+
548+
const cell = parseCellTri(cell_content);
549+
550+
// Check fuzzy match in id, name, description
551+
const id_lower = try allocLower(allocator, id);
552+
defer allocator.free(id_lower);
553+
const name_lower = try allocLower(allocator, cell.name);
554+
defer allocator.free(name_lower);
555+
const desc_lower = try allocLower(allocator, cell.description);
556+
defer allocator.free(desc_lower);
557+
558+
const matches_id = std.mem.indexOf(u8, id_lower, query_lower) != null;
559+
const matches_name = std.mem.indexOf(u8, name_lower, query_lower) != null;
560+
const matches_desc = std.mem.indexOf(u8, desc_lower, query_lower) != null;
561+
562+
if (!matches_id and !matches_name and !matches_desc) continue;
563+
564+
match_count += 1;
565+
const health = computeHealthScore(obj);
566+
const health_color = if (health >= 80) GREEN else if (health >= 50) YELLOW else RED;
567+
const status = jsonStr(obj, "status");
568+
const status_color = if (std.mem.eql(u8, status, "stable")) GREEN else YELLOW;
569+
570+
// Match indicator
571+
std.debug.print(" {s}{s}{s} ", .{ WHITE, cell.id, RESET });
572+
if (matches_id) std.debug.print("{s}[id]{s} ", .{ GREEN, RESET });
573+
if (matches_name) std.debug.print("{s}[name]{s} ", .{ CYAN, RESET });
574+
if (matches_desc) std.debug.print("{s}[desc]{s} ", .{ GRAY, RESET });
575+
std.debug.print("\n", .{});
576+
577+
std.debug.print(" Name: {s}{s}{s}\n", .{ WHITE, cell.name, RESET });
578+
if (cell.description.len > 0) {
579+
std.debug.print(" Desc: {s}{s}{s}\n", .{ GRAY, cell.description, RESET });
580+
}
581+
std.debug.print(" Health: {s}{d}%{s} | Status: {s}{s}{s}\n", .{
582+
health_color, health, RESET, status_color, status, RESET,
583+
});
584+
std.debug.print("\n", .{});
585+
}
586+
587+
if (match_count == 0) {
588+
std.debug.print(" {s}No matches found for \"{s}\"{s}\n\n", .{ GRAY, query, RESET });
589+
} else {
590+
std.debug.print(" {s}Found {d} cell(s){s}\n\n", .{ GREEN, match_count, RESET });
591+
}
592+
}
593+
594+
// ═══════════════════════════════════════════════════════════════════════════════
595+
// FIND — filter by capability (commands, exports, etc.)
596+
// ═══════════════════════════════════════════════════════════════════════════════
597+
598+
fn runFind(allocator: Allocator, args: []const []const u8) !void {
599+
// Parse flags
600+
var capability_filter: ?[]const u8 = null;
601+
var export_filter: ?[]const u8 = null;
602+
var command_filter: ?[]const u8 = null;
603+
var i: usize = 0;
604+
while (i < args.len) : (i += 1) {
605+
if (std.mem.eql(u8, args[i], "--capability") and i + 1 < args.len) {
606+
capability_filter = args[i + 1];
607+
i += 1;
608+
} else if (std.mem.eql(u8, args[i], "--export") and i + 1 < args.len) {
609+
export_filter = args[i + 1];
610+
i += 1;
611+
} else if (std.mem.eql(u8, args[i], "--command") and i + 1 < args.len) {
612+
command_filter = args[i + 1];
613+
i += 1;
614+
}
615+
}
616+
617+
if (capability_filter == null and export_filter == null and command_filter == null) {
618+
std.debug.print("{s}Usage:{s} tri cell find --capability <name>\n", .{ YELLOW, RESET });
619+
std.debug.print(" tri cell find --command <name>\n", .{});
620+
std.debug.print(" tri cell find --export <name>\n\n", .{});
621+
std.debug.print(" Examples:\n", .{});
622+
std.debug.print(" tri cell find --capability pipeline\n", .{});
623+
std.debug.print(" tri cell find --command build\n", .{});
624+
std.debug.print(" tri cell find --export runIdempotencyCommand\n\n", .{});
625+
return;
626+
}
627+
628+
const registry = try loadRegistry(allocator);
629+
defer allocator.free(registry);
630+
631+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, registry, .{}) catch {
632+
std.debug.print("{s}ERROR{s}: Failed to parse registry\n", .{ RED, RESET });
633+
return;
634+
};
635+
defer parsed.deinit();
636+
637+
const cells = (parsed.value.object.get("cells") orelse return).array.items;
638+
639+
std.debug.print("\n{s}🎯 FIND CELLS BY CAPABILITY{s}\n\n", .{ CYAN, RESET });
640+
641+
var match_count: usize = 0;
642+
643+
for (cells) |item| {
644+
const obj = item.object;
645+
const path = jsonStr(obj, "path");
646+
647+
// Load cell.tri for contributes
648+
const cell_tri_path = std.fmt.allocPrint(allocator, "{s}/cell.tri", .{path}) catch continue;
649+
defer allocator.free(cell_tri_path);
650+
651+
const cell_content = std.fs.cwd().readFileAlloc(allocator, cell_tri_path, 65536) catch continue;
652+
defer allocator.free(cell_content);
653+
654+
const cell = parseCellTri(cell_content);
655+
656+
var matches = false;
657+
var match_details: []const u8 = "";
658+
659+
// Check --capability filter (searches in capabilities array)
660+
if (capability_filter != null) {
661+
const cap = capability_filter.?;
662+
const cap_lower = try allocLower(allocator, cap);
663+
defer allocator.free(cap_lower);
664+
const caps_lower = try allocLower(allocator, cell.capabilities);
665+
defer allocator.free(caps_lower);
666+
667+
if (std.mem.indexOf(u8, caps_lower, cap_lower) != null) {
668+
matches = true;
669+
match_details = "capability";
670+
}
671+
}
672+
673+
// Check --command filter (searches in contributes.commands)
674+
if (!matches and command_filter != null) {
675+
const cmd = command_filter.?;
676+
var cmd_iter = cell_parser.ArrayIterator.init(cell.contributes_commands);
677+
while (cmd_iter.next()) |command| {
678+
if (std.mem.indexOf(u8, command, cmd) != null) {
679+
matches = true;
680+
match_details = "command";
681+
break;
682+
}
683+
}
684+
}
685+
686+
// Check --export filter (searches in contributes.exports)
687+
if (!matches and export_filter != null) {
688+
const exp = export_filter.?;
689+
var exp_iter = cell_parser.ArrayIterator.init(cell.contributes_exports);
690+
while (exp_iter.next()) |export_name| {
691+
if (std.mem.indexOf(u8, export_name, exp) != null) {
692+
matches = true;
693+
match_details = "export";
694+
break;
695+
}
696+
}
697+
}
698+
699+
if (!matches) continue;
700+
701+
match_count += 1;
702+
const health = computeHealthScore(obj);
703+
const health_color = if (health >= 80) GREEN else if (health >= 50) YELLOW else RED;
704+
705+
std.debug.print(" {s}{s}{s} ", .{ WHITE, cell.id, RESET });
706+
std.debug.print("{s}({s}){s}\n", .{ GRAY, match_details, RESET });
707+
std.debug.print(" Name: {s}\n", .{cell.name});
708+
709+
// Show matching details
710+
if (command_filter != null and cell.contributes_commands.len > 0) {
711+
std.debug.print(" Commands: {s}\n", .{cell.contributes_commands});
712+
}
713+
if (export_filter != null and cell.contributes_exports.len > 0) {
714+
std.debug.print(" Exports: {s}\n", .{cell.contributes_exports});
715+
}
716+
if (capability_filter != null and cell.capabilities.len > 0) {
717+
std.debug.print(" Capabilities: {s}\n", .{cell.capabilities});
718+
}
719+
720+
std.debug.print(" Health: {s}{d}%{s}\n", .{ health_color, health, RESET });
721+
std.debug.print("\n", .{});
722+
}
723+
724+
if (match_count == 0) {
725+
std.debug.print(" {s}No cells found with specified capability{s}\n\n", .{ GRAY, RESET });
726+
} else {
727+
std.debug.print(" {s}Found {d} cell(s){s}\n\n", .{ GREEN, match_count, RESET });
728+
}
729+
}
730+
477731
// ═══════════════════════════════════════════════════════════════════════════════
478732
// INFO — detailed view of a single cell
479733
// ═══════════════════════════════════════════════════════════════════════════════
@@ -7672,6 +7926,22 @@ fn findCellVersion(cells: []const std.json.Value, cell_id: []const u8) ?Version
76727926
return null;
76737927
}
76747928

7929+
// ═══════════════════════════════════════════════════════════════════════════════
7930+
// CASE-INSENSITIVE SEARCH HELPERS
7931+
// ═══════════════════════════════════════════════════════════════════════════════
7932+
7933+
fn toLower(c: u8) u8 {
7934+
return if (c >= 'A' and c <= 'Z') c + 32 else c;
7935+
}
7936+
7937+
fn allocLower(allocator: Allocator, s: []const u8) ![]u8 {
7938+
const result = try allocator.alloc(u8, s.len);
7939+
for (s, 0..) |c, i| {
7940+
result[i] = toLower(c);
7941+
}
7942+
return result;
7943+
}
7944+
76757945
// ═══════════════════════════════════════════════════════════════════════════════
76767946
// FILESYSTEM DISCOVERY — walk directories for cell.tri
76777947
// ═══════════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)