using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace AxCopilot.Services.Agent; public static class ProjectRulesService { public class ProjectRule { public string FilePath { get; set; } = ""; public string Name { get; set; } = ""; public string Description { get; set; } = ""; public string AppliesTo { get; set; } = ""; public string When { get; set; } = "always"; public string Body { get; set; } = ""; } private static readonly Regex FrontMatterRegex = new Regex("^---\\s*\\n(.*?)\\n---\\s*\\n", RegexOptions.Compiled | RegexOptions.Singleline); private static readonly Regex YamlKeyValue = new Regex("^\\s*(\\w[\\w-]*)\\s*:\\s*(.+?)\\s*$", RegexOptions.Multiline | RegexOptions.Compiled); public static List LoadRules(string workFolder) { List list = new List(); if (string.IsNullOrEmpty(workFolder)) { return list; } string text = FindRulesDirectory(workFolder); if (text == null) { return list; } try { string[] files = Directory.GetFiles(text, "*.md"); foreach (string filePath in files) { ProjectRule projectRule = ParseRuleFile(filePath); if (projectRule != null) { list.Add(projectRule); } } } catch { } return list; } public static List FilterRules(List rules, string when = "always", IEnumerable? filePaths = null) { List list = new List(); foreach (ProjectRule rule in rules) { string text = rule.When.ToLowerInvariant().Trim(); if (text != "always" && text != when.ToLowerInvariant()) { continue; } if (!string.IsNullOrEmpty(rule.AppliesTo) && filePaths != null) { string pattern = rule.AppliesTo.Trim(); if (!filePaths.Any((string fp) => MatchesGlob(fp, pattern))) { continue; } } list.Add(rule); } return list; } public static string FormatForSystemPrompt(List rules) { if (rules.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("\n## 프로젝트 규칙 (.ax/rules/)"); stringBuilder.AppendLine("아래 규칙을 반드시 준수하세요:\n"); foreach (ProjectRule rule in rules) { if (!string.IsNullOrEmpty(rule.Name)) { StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2); handler.AppendLiteral("### "); handler.AppendFormatted(rule.Name); stringBuilder3.AppendLine(ref handler); } if (!string.IsNullOrEmpty(rule.Description)) { StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); handler.AppendLiteral("*"); handler.AppendFormatted(rule.Description); handler.AppendLiteral("*\n"); stringBuilder4.AppendLine(ref handler); } stringBuilder.AppendLine(rule.Body.Trim()); stringBuilder.AppendLine(); } return stringBuilder.ToString(); } internal static string? FindRulesDirectory(string workFolder) { string text = workFolder; for (int i = 0; i < 3; i++) { if (string.IsNullOrEmpty(text)) { break; } string text2 = Path.Combine(text, ".ax", "rules"); if (Directory.Exists(text2)) { return text2; } text = Directory.GetParent(text)?.FullName; } return null; } internal static ProjectRule? ParseRuleFile(string filePath) { try { string text = File.ReadAllText(filePath, Encoding.UTF8); if (string.IsNullOrWhiteSpace(text)) { return null; } ProjectRule projectRule = new ProjectRule { FilePath = filePath, Name = Path.GetFileNameWithoutExtension(filePath) }; Match match = FrontMatterRegex.Match(text); if (match.Success) { string value = match.Groups[1].Value; foreach (Match item in YamlKeyValue.Matches(value)) { string text2 = item.Groups[1].Value.ToLowerInvariant(); string text3 = item.Groups[2].Value.Trim().Trim('"', '\''); switch (text2) { case "name": projectRule.Name = text3; break; case "description": projectRule.Description = text3; break; case "applies-to": case "appliesto": projectRule.AppliesTo = text3; break; case "when": projectRule.When = text3; break; } } string text4 = text; int num = match.Index + match.Length; projectRule.Body = text4.Substring(num, text4.Length - num); } else { projectRule.Body = text; } return string.IsNullOrWhiteSpace(projectRule.Body) ? null : projectRule; } catch { return null; } } private static bool MatchesGlob(string path, string pattern) { if (pattern.StartsWith("*.")) { string text = pattern; return path.EndsWith(text.Substring(1, text.Length - 1), StringComparison.OrdinalIgnoreCase); } if (pattern.StartsWith("**/")) { string text = pattern; string pattern2 = text.Substring(3, text.Length - 3); return MatchesGlob(Path.GetFileName(path), pattern2); } return Path.GetFileName(path).Equals(pattern, StringComparison.OrdinalIgnoreCase); } }