Forge
csharp4405de34
1using System.Text;
2using System.Text.Json;
3using System.Text.RegularExpressions;
4using CascadeIDE.Services;
5using Tomlyn;
6using Tomlyn.Model;
7
8/// <summary>Добавляет domain/object/intent/path_role в <c>intent-catalog.toml</c> (ADR 0154).</summary>
9internal static class SlashCatalogSemanticAnnotator
10{
11 private static readonly TomlSerializerOptions TomlOptions = new()
12 {
13 PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
14 };
15
16 private static readonly Regex PathLine =
17 new(@"^\s*path\s*=\s*""(?<path>[^""]+)""\s*$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
18
19 private static readonly Regex DomainLine =
20 new(@"^\s*domain\s*=", RegexOptions.Compiled | RegexOptions.CultureInvariant);
21
22 public static int AnnotateFile(string catalogPath)
23 {
24 var text = File.ReadAllText(catalogPath, Encoding.UTF8);
25 var lines = text.Replace("\r\n", "\n").Split('\n');
26 var mapLevels = loadMapLevels(catalogPath);
27 var changed = 0;
28 var i = 0;
29
30 while (i < lines.Length)
31 {
32 if (!lines[i].Trim().Equals("[[command.form.slash]]", StringComparison.Ordinal))
33 {
34 i++;
35 continue;
36 }
37
38 var blockEnd = findBlockEnd(lines, i + 1);
39 if (!tryFindPath(lines, i + 1, blockEnd, out var pathLine, out var path))
40 {
41 i = blockEnd;
42 continue;
43 }
44
45 if (blockHasDomain(lines, i + 1, blockEnd))
46 {
47 i = blockEnd;
48 continue;
49 }
50
51 mapLevels.TryGetValue(path, out var mapLevel);
52 var fields = SlashRouteSemantics.Resolve(path, mapLevel);
53 var insert = buildInsertLines(fields);
54 lines = insertLines(lines, pathLine + 1, insert);
55 blockEnd += insert.Length;
56 changed++;
57 i = blockEnd;
58 }
59
60 if (changed > 0)
61 {
62 var outText = string.Join(Environment.NewLine, lines);
63 if (text.EndsWith('\n') && !outText.EndsWith(Environment.NewLine, StringComparison.Ordinal))
64 outText += Environment.NewLine;
65 File.WriteAllText(catalogPath, outText, Encoding.UTF8);
66 }
67
68 Console.WriteLine($"Annotated {changed} slash block(s) in {catalogPath}");
69 return changed;
70 }
71
72 private static Dictionary<string, string?> loadMapLevels(string catalogPath)
73 {
74 var result = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
75 var text = File.ReadAllText(catalogPath, Encoding.UTF8);
76 if (TomlSerializer.Deserialize<TomlTable>(text, TomlOptions) is not TomlTable root)
77 return result;
78
79 if (!root.TryGetValue("command", out var commandNode))
80 return result;
81
82 collectMapLevels(commandNode, result);
83 return result;
84 }
85
86 private static void collectMapLevels(object? commandNode, Dictionary<string, string?> mapLevels)
87 {
88 switch (commandNode)
89 {
90 case TomlTableArray commands:
91 foreach (var command in commands)
92 collectSlashRows(command, mapLevels);
93 break;
94 case TomlTable single:
95 collectSlashRows(single, mapLevels);
96 break;
97 }
98 }
99
100 private static void collectSlashRows(TomlTable command, Dictionary<string, string?> mapLevels)
101 {
102 if (command.TryGetValue("slash", out var slashNode))
103 walkSlashRows(slashNode, mapLevels);
104
105 if (command.TryGetValue("form", out var formNode) && formNode is TomlTable form
106 && form.TryGetValue("slash", out var formSlash))
107 walkSlashRows(formSlash, mapLevels);
108 }
109
110 private static void walkSlashRows(object? slashNode, Dictionary<string, string?> mapLevels)
111 {
112 switch (slashNode)
113 {
114 case TomlTableArray rows:
115 foreach (var row in rows)
116 addMapLevel(row, mapLevels);
117 break;
118 case TomlTable single:
119 addMapLevel(single, mapLevels);
120 break;
121 }
122 }
123
124 private static void addMapLevel(TomlTable row, Dictionary<string, string?> mapLevels)
125 {
126 if (!row.TryGetValue("path", out var pathNode))
127 return;
128
129 var path = IntentCatalogSlashPathCollector.normalizeSlashPath(pathNode.ToString());
130 if (path.Length == 0)
131 return;
132
133 string? level = null;
134 if (row.TryGetValue("args", out var argsNode) && argsNode is TomlTable args
135 && args.TryGetValue("level", out var levelNode))
136 {
137 level = levelNode.ToString()?.Trim();
138 }
139
140 mapLevels[path] = level;
141 }
142
143 private static int findBlockEnd(string[] lines, int start)
144 {
145 for (var i = start; i < lines.Length; i++)
146 {
147 var t = lines[i].Trim();
148 if (t.StartsWith("[[", StringComparison.Ordinal) && !t.Equals("[[command.form.slash]]", StringComparison.Ordinal))
149 return i;
150 if (t.StartsWith("# ===", StringComparison.Ordinal))
151 return i;
152 }
153
154 return lines.Length;
155 }
156
157 private static bool tryFindPath(string[] lines, int start, int end, out int pathLine, out string path)
158 {
159 pathLine = -1;
160 path = "";
161 for (var i = start; i < end; i++)
162 {
163 var m = PathLine.Match(lines[i]);
164 if (!m.Success)
165 continue;
166
167 pathLine = i;
168 path = IntentCatalogSlashPathCollector.normalizeSlashPath(m.Groups["path"].Value);
169 return path.Length > 0;
170 }
171
172 return false;
173 }
174
175 private static bool blockHasDomain(string[] lines, int start, int end)
176 {
177 for (var i = start; i < end; i++)
178 {
179 if (DomainLine.IsMatch(lines[i]))
180 return true;
181 }
182
183 return false;
184 }
185
186 private static string[] buildInsertLines(SlashSemanticFields fields)
187 {
188 var role = fields.PathRole == SlashPathRole.Alias ? "alias" : "canonical";
189 var list = new List<string>
190 {
191 $"domain = \"{fields.Domain}\"",
192 };
193
194 if (!string.IsNullOrEmpty(fields.Object))
195 list.Add($"object = \"{fields.Object}\"");
196
197 if (!string.IsNullOrEmpty(fields.Intent))
198 list.Add($"intent = \"{fields.Intent}\"");
199
200 if (fields.PathRole == SlashPathRole.Alias)
201 list.Add($"path_role = \"{role}\"");
202
203 return list.ToArray();
204 }
205
206 private static string[] insertLines(string[] lines, int index, string[] insert)
207 {
208 var result = new string[lines.Length + insert.Length];
209 Array.Copy(lines, 0, result, 0, index);
210 Array.Copy(insert, 0, result, index, insert.Length);
211 Array.Copy(lines, index, result, index + insert.Length, lines.Length - index);
212 return result;
213 }
214}
215
View only · write via MCP/CIDE