Editor/Contracts/SboxCapabilityCatalog.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;

namespace Editor.Mcp
{
	internal sealed record SboxCapabilityMetadata(
		string Tool,
		string Category,
		string Summary,
		string AccessMode,
		bool RequiresApproval,
		string[] RequiredEditorStates,
		string Verification,
		string[] Keywords)
	{
		public object ToPayload() => new
		{
			Tool,
			Category,
			Summary,
			AccessMode,
			RequiresApproval,
			RequiredEditorStates,
			Verification,
			Keywords
		};
	}

	internal static class SboxCapabilityCatalog
	{
		private static readonly IReadOnlyList<SboxCapabilityMetadata> Items = new[]
		{
			Read("pocketknife_find_capability", "discovery", "Find a small ranked set of editor tools by task terms before loading a tool schema.", Array.Empty<string>(), "Results are ranked from explicit metadata and do not execute a tool.", "capability", "search", "routing", "tool"),
			Read("pocketknife_query_schema", "api", "Search the official s&box API schema for types and members.", new[] { "editor-process" }, "The response identifies the official schema source and cache timestamp.", "sbox", "api", "schema", "type"),
			Read("pocketknife_search_local_references", "reference", "Search the approved local reference index for code, systems, assets, and documentation.", new[] { "editor-process" }, "Results include bounded snippets and indexed source metadata.", "reference", "vault", "code", "asset", "system"),
			Write("pocketknife_rebuild_local_reference_index", "reference", "Rebuild the local reference index from its configured catalog.", new[] { "editor-process" }, "The index reports the scanned source set and resulting entry count.", "reference", "index", "rebuild"),
			Read("pocketknife_get_local_reference", "reference", "Read one bounded local reference result by stable ID.", new[] { "editor-process" }, "The returned item is resolved from the local reference index.", "reference", "read", "source"),
			Read("pocketknife_search_live_codesearch", "api", "Search public s&box Code Search results through the official page or local worker.", new[] { "editor-process" }, "The response retains official package/file URLs and transport status.", "sbox", "code", "search", "public"),
			Analyze("pocketknife_test_clothing", "asset", "Load a clothing asset into the configured test harness for inspection.", new[] { "editor-process", "active-project" }, "The result reports the selected asset and test-harness outcome.", "clothing", "avatar", "asset"),
			External("pocketknife_open_publish_wizard", "publishing", "Open the s&box publishing workflow for the active project.", new[] { "editor-process", "active-project" }, "The editor acceptance is reported; publication itself requires a separate explicit workflow.", "publish", "external", "package"),
			Write("pocketknife_spawn_procedural_road", "procedural", "Create a project road component using the loaded s&box runtime types.", new[] { "editor-process", "active-scene" }, "The created object and component type are returned.", "road", "procedural", "scene"),
			Write("pocketknife_spawn_water_surface", "procedural", "Create a project water surface component using the loaded runtime types.", new[] { "editor-process", "active-scene" }, "The created object and component type are returned.", "water", "procedural", "scene"),
			Write("pocketknife_spawn_architecture_plan", "procedural", "Create a bounded architecture plan in the active scene.", new[] { "editor-process", "active-scene" }, "The plan object and requested dimensions are returned.", "architecture", "building", "procedural"),
			Write("pocketknife_add_architecture_room", "procedural", "Add a room to an existing architecture plan.", new[] { "editor-process", "active-scene" }, "The parent, room, position, and dimensions are returned.", "architecture", "room", "procedural"),
			Write("pocketknife_navmesh_bake", "navigation", "Bake the active scene navigation mesh when its dependency is available.", new[] { "editor-process", "active-scene" }, "The navigation mesh leaves its generating/dirty state before success is reported.", "navmesh", "navigation", "bake"),
			Write("pocketknife_switch_editor_tab", "editor", "Open or activate a named editor surface when its state can be verified.", new[] { "editor-process" }, "The matching surface or dock activation is checked after the request.", "editor", "tab", "surface"),
			Read("pocketknife_dock_state", "editor", "List bounded editor dock state including open, active, and raised readback.", new[] { "editor-process" }, "Dock identity and state are returned from Editor.DockManager.", "editor", "dock", "state"),
			Write("pocketknife_configure_navmesh", "navigation", "Enable or disable the active scene navigation mesh and optionally save it.", new[] { "editor-process", "active-scene" }, "The requested enabled state and save result are read back.", "navmesh", "navigation", "configure"),
			Analyze("pocketknife_find_type", "code", "Find loaded s&box types by full name, short name, or fragment.", new[] { "editor-process" }, "The result lists matching loaded types and a bounded count.", "code", "type", "reflection"),
			Analyze("pocketknife_type_members", "code", "Inspect properties and methods for one loaded s&box type.", new[] { "editor-process" }, "The returned member list is derived from loaded runtime assemblies.", "code", "type", "members"),
			Write("pocketknife_compile_await", "build", "Await project compiler freshness after a compile dispatch.", new[] { "editor-process", "active-project" }, "Baseline and observed compiler revisions plus settled diagnostics are returned.", "compile", "build", "freshness"),
			Read("pocketknife_package_references", "project", "Read package references from the active project's .sbproj.", new[] { "editor-process", "active-project" }, "The selected project file and parsed package references are returned.", "project", "packages", "dependencies"),
			Write("pocketknife_open_in_hammer", "asset", "Open a .vmap asset in the Hammer editor when surface state is verifiable.", new[] { "editor-process", "active-project" }, "The selected asset path and active-surface verification state are returned.", "hammer", "vmap", "editor"),
			Write("pocketknife_edit_material", "asset", "Open a .vmat asset in the material editor when surface state is verifiable.", new[] { "editor-process", "active-project" }, "The selected material path and active-surface verification state are returned.", "material", "vmat", "editor"),
			Write("pocketknife_create_sound_event", "asset", "Create a bounded .sound event asset from an approved audio path.", new[] { "editor-process", "active-project" }, "The asset path and editor refresh result are verified.", "sound", "audio", "asset"),
			Read("pocketknife_asset_search", "asset", "Search the editor asset index with bounded kind and path filters.", new[] { "editor-process", "active-project" }, "Results include count, pagination, source, and provenance.", "asset", "search", "project"),
			Write("pocketknife_paint_terrain", "scene", "Create the supported terrain component for a bounded terrain request.", new[] { "editor-process", "active-scene" }, "The created component is returned; unsupported paint APIs remain unavailable.", "terrain", "scene", "component"),
			Write("pocketknife_spawn_sandbox_entity", "scene", "Create a supported sandbox prop entity from a project model asset.", new[] { "editor-process", "active-scene" }, "The object, renderer, collider, and physics state are returned.", "entity", "prop", "physics"),
			Read("pocketknife_scene_sessions", "editor", "List open scene sessions with stable IDs and lifecycle state.", new[] { "editor-process" }, "Session ID, path, active, dirty, mounted, and playing state are returned.", "scene", "session", "tabs"),
			Write("pocketknife_scene_activate", "editor", "Activate an existing scene session by stable ID or path.", new[] { "editor-process" }, "SceneEditorSession.Active is read back before success.", "scene", "session", "activate"),
			Analyze("pocketknife_viewport_capture", "observability", "Capture an allowlisted editor or scene camera mode with verified PNG output.", new[] { "editor-process" }, "Mode, dimensions, bytes, file, and image block are verified.", "screenshot", "viewport", "camera"),
			Read("pocketknife_package_search", "package", "Search published packages with bounded provenance-aware results.", new[] { "editor-process" }, "Package ident, title, author, type, source, and provenance are returned.", "package", "cloud", "search"),
			Write("pocketknife_package_install", "package", "Install one published package through the typed editor asset system.", new[] { "editor-process", "active-project" }, "Installed revision and dependency state are read back.", "package", "cloud", "install"),
			Read("pocketknife_package_status", "package", "Read cloud package metadata and local installed revision state.", new[] { "editor-process" }, "Installed state and revision are returned without exposing credentials.", "package", "cloud", "status"),
			Read("pocketknife_asset_info", "asset", "Read indexed asset or package provenance and compile state.", new[] { "editor-process" }, "Resource path, source, package, revision, compile state, and surface are returned.", "asset", "info", "provenance"),
			Read("pocketknife_editor_surface_status", "editor", "Report runtime availability and dependencies for an allowlisted editor surface.", new[] { "editor-process" }, "Runtime type, official matches, and missing dependency code are returned.", "surface", "editor", "dependency"),
			Write("pocketknife_editor_document_open", "editor", "Open an allowlisted editor document only when active-surface state can be checked.", new[] { "editor-process", "active-project" }, "OpenFile dispatch is not Ready without surface readback.", "document", "surface", "open"),
			Read("pocketknife_navmesh_status", "navigation", "Read typed NavMesh state for an open scene session.", new[] { "editor-process", "active-scene" }, "Enabled, generating, dirty, scene, and dependency state are returned.", "navmesh", "navigation", "status"),
			Read("pocketknife_navmesh_path", "navigation", "Calculate a bounded path through the active typed NavMesh.", new[] { "editor-process", "active-scene" }, "Returned points are capped and tied to the active scene.", "navmesh", "navigation", "path"),
			Read("pocketknife_hotload_await", "build", "Await a typed editor hotload manager state transition.", new[] { "editor-process" }, "Baseline and observed hotload revisions plus pending state are returned.", "hotload", "build", "freshness"),
			Read("pocketknife_network_status", "network", "Read redacted typed editor and provider availability state.", new[] { "editor-process" }, "No credentials or unrestricted endpoints are returned.", "network", "status", "redacted"),
			Write("pocketknife_lobby_start", "network", "Start a typed runtime lobby when the current runtime exposes one.", new[] { "editor-process" }, "Lobby identity and state are read back before success.", "lobby", "network", "start"),
			Write("pocketknife_lobby_stop", "network", "Stop a typed runtime lobby when the current runtime exposes one.", new[] { "editor-process" }, "Lobby stopped state is read back before success.", "lobby", "network", "stop"),
			Write("pocketknife_server_start", "provider", "Start one typed Voyageur server operation.", new[] { "editor-process" }, "Provider operation ID and redacted state are verified.", "server", "voyageur", "start"),
			Read("pocketknife_server_status", "provider", "Read one typed Voyageur server operation state.", new[] { "editor-process" }, "Provider operation ID and redacted state are returned.", "server", "voyageur", "status"),
			Write("pocketknife_server_stop", "provider", "Stop one typed Voyageur server operation.", new[] { "editor-process" }, "Stopped provider state is verified.", "server", "voyageur", "stop")
		};

		public static IReadOnlyList<SboxCapabilityMetadata> All => Items;

		public static SboxCapabilityMetadata? Find(string tool)
			=> Items.FirstOrDefault(item => item.Tool.Equals(tool?.Trim(), StringComparison.OrdinalIgnoreCase));

		public static IReadOnlyList<SboxCapabilityMetadata> Search(string query, int limit, int offset, out int total)
		{
			var normalized = (query ?? "").Trim().ToLowerInvariant();
			var terms = normalized.Split(new[] { ' ', '\t', '\r', '\n', '/', '\\', '.', ',', ':', ';', '-', '_' }, StringSplitOptions.RemoveEmptyEntries).Where(term => term.Length >= 2).ToArray();
			var ranked = Items
				.Select(item => new { Item = item, Score = Score(item, normalized, terms) })
				.Where(result => terms.Length == 0 || result.Score > 0)
				.OrderByDescending(result => result.Score)
				.ThenBy(result => result.Item.Tool, StringComparer.OrdinalIgnoreCase)
				.ToArray();
			 total = ranked.Length;
			return ranked.Skip(Math.Max(offset, 0)).Take(Math.Clamp(limit, 1, 20)).Select(result => result.Item).ToArray();
		}

		private static int Score(SboxCapabilityMetadata item, string query, string[] terms)
		{
			if ( terms.Length == 0 ) return 1;
			var tool = item.Tool.ToLowerInvariant();
			var category = item.Category.ToLowerInvariant();
			var summary = item.Summary.ToLowerInvariant();
			var keywords = item.Keywords.Select(keyword => keyword.ToLowerInvariant()).ToArray();
			var score = 0;
			foreach ( var term in terms )
			{
				if ( tool.Contains(term, StringComparison.Ordinal) ) score += 8;
				if ( category.Contains(term, StringComparison.Ordinal) ) score += 5;
				if ( summary.Contains(term, StringComparison.Ordinal) ) score += 2;
				if ( keywords.Any(keyword => keyword.Contains(term, StringComparison.Ordinal)) ) score += 4;
			}
			if ( !string.IsNullOrWhiteSpace(query) && summary.Contains(query, StringComparison.Ordinal) ) score += 3;
			return score;
		}

		private static SboxCapabilityMetadata Read(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
			=> new(tool, category, summary, "read", false, states, verification, keywords);

		private static SboxCapabilityMetadata Analyze(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
			=> new(tool, category, summary, "analyze", false, states, verification, keywords);

		private static SboxCapabilityMetadata Write(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
			=> new(tool, category, summary, "project-write", true, states, verification, keywords);

		private static SboxCapabilityMetadata External(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
			=> new(tool, category, summary, "external-write", true, states, verification, keywords);
	}
}