210 lines
5.3 KiB
C#
210 lines
5.3 KiB
C#
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<ProjectRule> LoadRules(string workFolder)
|
|
{
|
|
List<ProjectRule> list = new List<ProjectRule>();
|
|
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<ProjectRule> FilterRules(List<ProjectRule> rules, string when = "always", IEnumerable<string>? filePaths = null)
|
|
{
|
|
List<ProjectRule> list = new List<ProjectRule>();
|
|
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<ProjectRule> 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);
|
|
}
|
|
}
|