Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Diagnostics;
4using System.Text.Json;
5using System.Text.Json.Serialization;
6using CascadeIDE.Features.Workspace;
7using CascadeIDE.Features.Workspace.DataAcquisition;
8using CascadeIDE.Services;
9
10namespace CascadeIDE.Features.CasaField.Application;
11
12/// <summary>Resolve CASA agent store from <c>.cascade/workspace.toml</c> [workspace.casa_field].</summary>
13public static class CasaFieldStoreResolver
14{
15 public static string? ResolveStoreDirectory(string? workspaceRoot)
16 {
17 if (string.IsNullOrWhiteSpace(workspaceRoot))
18 return null;
19
20 var toml = RepositoryWorkspaceTomlLoader.TryLoad(workspaceRoot);
21 var rel = toml?.Workspace?.CasaField?.StoreDir?.Trim();
22 if (string.IsNullOrEmpty(rel))
23 rel = "../casa-ontology-payload/examples/agent-stores/research-agent-lab-v0";
24
25 return ResolvePath(workspaceRoot, rel);
26 }
27
28 public static string? ResolveFieldStatePath(string? workspaceRoot)
29 {
30 var store = ResolveStoreDirectory(workspaceRoot);
31 return store is null ? null : Path.Combine(store, "field_state.json");
32 }
33
34 public static string? ResolvePath(string workspaceRoot, string path)
35 {
36 if (Path.IsPathRooted(path))
37 return Path.GetFullPath(path);
38
39 return Path.GetFullPath(Path.Combine(workspaceRoot.Trim(), path.Replace('/', Path.DirectorySeparatorChar)));
40 }
41}
42
43public sealed record CasaFieldTarget(
44 string ConceptId,
45 string Kind,
46 string? DocPath,
47 string? Section,
48 int? SectionLine,
49 string? CodeFile,
50 int? CodeLine,
51 string? TextPreview);
52
53public sealed record CasaFieldQueryResult(
54 string Query,
55 double WallMs,
56 string? BundleName,
57 int? FieldVersion,
58 bool? Stale,
59 IReadOnlyList<CasaFieldTarget> Targets,
60 string? Error);
61
62/// <summary>Query agent field store: match claims → KB doc + section (no LLM).</summary>
63public static class CasaFieldQueryResolver
64{
65 private static readonly JsonSerializerOptions JsonOptions = new()
66 {
67 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
68 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
69 WriteIndented = false,
70 };
71
72 public static string BuildJson(string? workspaceRoot, string query)
73 {
74 var result = Query(workspaceRoot, query);
75 return JsonSerializer.Serialize(result, JsonOptions);
76 }
77
78 public static CasaFieldQueryResult Query(string? workspaceRoot, string query)
79 {
80 var sw = Stopwatch.StartNew();
81 if (string.IsNullOrWhiteSpace(workspaceRoot))
82 return new CasaFieldQueryResult(query, 0, null, null, null, [], "no_workspace");
83
84 var fieldPath = CasaFieldStoreResolver.ResolveFieldStatePath(workspaceRoot);
85 if (fieldPath is null || !File.Exists(fieldPath))
86 return new CasaFieldQueryResult(query, sw.Elapsed.TotalMilliseconds, null, null, null, [], "field_state_missing");
87
88 var storeDir = Path.GetDirectoryName(fieldPath)!;
89 try
90 {
91 using var doc = JsonDocument.Parse(File.ReadAllText(fieldPath));
92 var root = doc.RootElement;
93 var bundleName = root.TryGetProperty("grid", out var grid)
94 && grid.TryGetProperty("source_bundle", out var sb)
95 ? sb.GetString()
96 : null;
97
98 var navItems = ReadNavItems(root, storeDir, bundleName);
99 var fieldVersion = root.TryGetProperty("field_version", out var fv) && fv.ValueKind == JsonValueKind.Number
100 ? fv.GetInt32()
101 : (int?)null;
102 var stale = IsFieldStale(root, storeDir);
103 var targets = MatchQuery(query, navItems, workspaceRoot);
104 sw.Stop();
105 return new CasaFieldQueryResult(query, Math.Round(sw.Elapsed.TotalMilliseconds, 2), bundleName, fieldVersion, stale, targets, null);
106 }
107 catch (Exception ex)
108 {
109 sw.Stop();
110 return new CasaFieldQueryResult(query, sw.Elapsed.TotalMilliseconds, null, null, null, [], ex.Message);
111 }
112 }
113
114 private sealed record NavItem(
115 string ConceptId,
116 string DocPath,
117 string Section,
118 string Text,
119 IReadOnlyList<(string File, int? Line)> CodeAnchors);
120
121 private static bool IsFieldStale(JsonElement root, string storeDir)
122 {
123 if (!root.TryGetProperty("bundles", out var bundles) || bundles.ValueKind != JsonValueKind.Object)
124 return false;
125 foreach (var prop in bundles.EnumerateObject())
126 {
127 if (!File.Exists(Path.Combine(storeDir, "bundles", prop.Name)))
128 return true;
129 }
130 return false;
131 }
132
133 private static List<NavItem> ReadNavItems(
134 JsonElement root,
135 string storeDir,
136 string? bundleName)
137 {
138 if (root.TryGetProperty("claims_nav", out var nav) && nav.ValueKind == JsonValueKind.Array)
139 return ParseNavArray(nav);
140
141 if (!string.IsNullOrEmpty(bundleName))
142 {
143 var bundlePath = Path.Combine(storeDir, "bundles", bundleName);
144 if (File.Exists(bundlePath))
145 {
146 using var bdoc = JsonDocument.Parse(File.ReadAllText(bundlePath));
147 if (bdoc.RootElement.TryGetProperty("claims", out var claims))
148 return ParseClaims(claims);
149 }
150 }
151
152 return [];
153 }
154
155 private static List<NavItem> ParseNavArray(JsonElement nav)
156 {
157 var list = new List<NavItem>();
158 foreach (var item in nav.EnumerateArray())
159 {
160 var cid = item.TryGetProperty("concept_id", out var c) ? c.GetString() : null;
161 var dp = item.TryGetProperty("doc_path", out var d) ? d.GetString() : null;
162 var sec = item.TryGetProperty("section", out var s) ? s.GetString() ?? "" : "";
163 if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(dp))
164 continue;
165 list.Add(new NavItem(cid!, dp!, sec, "", ParseCodeAnchors(item)));
166 }
167
168 return list;
169 }
170
171 private static List<NavItem> ParseClaims(JsonElement claims)
172 {
173 var list = new List<NavItem>();
174 foreach (var c in claims.EnumerateArray())
175 {
176 var cid = c.TryGetProperty("concept_id", out var ci) ? ci.GetString() : null;
177 var dp = c.TryGetProperty("doc_path", out var di) ? di.GetString() : null;
178 var sec = c.TryGetProperty("section", out var si) ? si.GetString() ?? "" : "";
179 var text = c.TryGetProperty("text", out var ti) ? ti.GetString() ?? "" : "";
180 if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(dp))
181 continue;
182 list.Add(new NavItem(cid!, dp!, sec, text, ParseCodeAnchors(c)));
183 }
184
185 return list;
186 }
187
188 private static IReadOnlyList<(string File, int? Line)> ParseCodeAnchors(JsonElement item)
189 {
190 if (!item.TryGetProperty("code_anchors", out var arr) || arr.ValueKind != JsonValueKind.Array)
191 return [];
192
193 var list = new List<(string, int?)>();
194 foreach (var a in arr.EnumerateArray())
195 {
196 var file = a.TryGetProperty("file", out var f) ? f.GetString() : null;
197 if (string.IsNullOrWhiteSpace(file))
198 continue;
199 int? line = a.TryGetProperty("line", out var ln) && ln.ValueKind == JsonValueKind.Number ? ln.GetInt32() : null;
200 list.Add((file!, line));
201 }
202
203 return list;
204 }
205
206 private static List<CasaFieldTarget> MatchQuery(
207 string query,
208 List<NavItem> items,
209 string workspaceRoot)
210 {
211 var q = query.Trim();
212 var words = q.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
213 .Where(w => w.Length > 2)
214 .Select(w => w.ToLowerInvariant())
215 .ToArray();
216
217 var hits = new List<(int Score, CasaFieldTarget Target)>();
218
219 foreach (var item in items)
220 {
221 var score = 0;
222 var cidL = item.ConceptId.ToLowerInvariant();
223 var secL = item.Section.ToLowerInvariant();
224 var textL = item.Text.ToLowerInvariant();
225 var qL = q.ToLowerInvariant();
226
227 if (cidL.Contains(qL, StringComparison.Ordinal) || qL.Contains(cidL, StringComparison.Ordinal))
228 score += 10;
229
230 foreach (var w in words)
231 {
232 if (cidL.Contains(w, StringComparison.Ordinal)) score += 3;
233 if (secL.Contains(w, StringComparison.Ordinal)) score += 2;
234 if (textL.Contains(w, StringComparison.Ordinal)) score += 2;
235 }
236
237 if (score <= 0)
238 continue;
239
240 var preview = item.Text.Length > 120 ? item.Text[..117] + "…" : item.Text;
241 var line = KbSectionLineResolver.TryFindSectionLine(workspaceRoot, item.DocPath, item.Section);
242 hits.Add((score, new CasaFieldTarget(item.ConceptId, "kb", item.DocPath, item.Section, line, null, null, preview)));
243
244 foreach (var (file, codeLine) in item.CodeAnchors)
245 {
246 hits.Add((score, new CasaFieldTarget(item.ConceptId, "code", null, null, null, file, codeLine, preview)));
247 }
248 }
249
250 return hits
251 .OrderByDescending(h => h.Score)
252 .ThenBy(h => h.Target.Kind, StringComparer.Ordinal)
253 .ThenBy(h => h.Target.ConceptId, StringComparer.OrdinalIgnoreCase)
254 .Select(h => h.Target)
255 .Take(8)
256 .ToList();
257 }
258}
259
View only · write via MCP/CIDE