메모리 규칙 경로 범위 적용을 위한 paths 프런트매터 지원 추가
Some checks failed
Release Gate / gate (push) Has been cancelled

계층형 메모리 문서에 YAML 유사 paths 프런트매터를 추가해 현재 작업 폴더 경로에 따라 규칙 적용 여부를 제어할 수 있도록 했습니다.

AgentMemoryService에서 프런트매터를 파싱하고 프로젝트 루트 기준 상대 경로에 대해 *, **, ? glob 매칭을 수행하도록 구현했습니다.

README와 DEVELOPMENT 문서에 메모리 규칙 범위 제어 기능과 동작 방식을 2026-04-07 00:22 (KST) 기준으로 반영했고, Release 빌드 경고 0 오류 0을 확인했습니다.
This commit is contained in:
2026-04-07 00:07:32 +09:00
parent 18551a0aea
commit 2e0362a88f
3 changed files with 107 additions and 1 deletions

View File

@@ -405,12 +405,17 @@ public class AgentMemoryService
if (string.IsNullOrWhiteSpace(content))
return;
var frontMatter = ParseFrontMatter(content);
if (frontMatter.Paths.Count > 0 && !ShouldApplyToCurrentWorkFolder(frontMatter.Paths, projectRoot, _currentWorkFolder))
return;
_instructionDocuments.Add(new MemoryInstructionDocument
{
Layer = layer,
Label = label,
Path = fullPath,
Content = content.Trim()
Content = frontMatter.Content.Trim(),
Paths = frontMatter.Paths
});
}
catch (Exception ex)
@@ -613,6 +618,91 @@ public class AgentMemoryService
}
}
private static (string Content, List<string> Paths) ParseFrontMatter(string content)
{
var lines = content.Replace("\r\n", "\n").Split('\n').ToList();
if (lines.Count < 3 || !string.Equals(lines[0].Trim(), "---", StringComparison.Ordinal))
return (content, new List<string>());
var endIndex = -1;
for (var i = 1; i < lines.Count; i++)
{
if (string.Equals(lines[i].Trim(), "---", StringComparison.Ordinal))
{
endIndex = i;
break;
}
}
if (endIndex < 1)
return (content, new List<string>());
var paths = new List<string>();
var inPaths = false;
for (var i = 1; i < endIndex; i++)
{
var line = lines[i].Trim();
if (line.StartsWith("paths:", StringComparison.OrdinalIgnoreCase))
{
inPaths = true;
continue;
}
if (inPaths)
{
if (line.StartsWith("-", StringComparison.Ordinal))
{
var pattern = line[1..].Trim().Trim('"');
if (!string.IsNullOrWhiteSpace(pattern))
paths.Add(pattern);
continue;
}
if (line.Length > 0)
inPaths = false;
}
}
var stripped = string.Join("\n", lines.Skip(endIndex + 1)).Trim();
return (stripped, paths);
}
private static bool ShouldApplyToCurrentWorkFolder(IReadOnlyList<string> patterns, string? projectRoot, string? currentWorkFolder)
{
if (patterns.Count == 0)
return true;
if (string.IsNullOrWhiteSpace(projectRoot) || string.IsNullOrWhiteSpace(currentWorkFolder))
return false;
var relative = Path.GetRelativePath(projectRoot, currentWorkFolder).Replace('\\', '/');
if (string.Equals(relative, ".", StringComparison.Ordinal))
relative = "";
return patterns.Any(pattern => GlobMatches(relative, pattern));
}
private static bool GlobMatches(string relativePath, string pattern)
{
var normalizedPattern = (pattern ?? "").Replace('\\', '/').Trim();
var normalizedPath = (relativePath ?? "").Replace('\\', '/').Trim('/');
if (string.IsNullOrWhiteSpace(normalizedPattern))
return false;
var regex = "^" + System.Text.RegularExpressions.Regex.Escape(normalizedPattern)
.Replace(@"\*\*", "§§DOUBLESTAR§§")
.Replace(@"\*", "[^/]*")
.Replace(@"\?", "[^/]")
.Replace("§§DOUBLESTAR§§", ".*")
+ "$";
if (System.Text.RegularExpressions.Regex.IsMatch(normalizedPath, regex, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
return true;
if (!string.IsNullOrEmpty(normalizedPath))
normalizedPath += "/";
return System.Text.RegularExpressions.Regex.IsMatch(normalizedPath, regex, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
}
/// <summary>텍스트를 토큰으로 분리합니다.</summary>
private static HashSet<string> Tokenize(string text)
{
@@ -680,4 +770,7 @@ public class MemoryInstructionDocument
[JsonPropertyName("content")]
public string Content { get; set; } = "";
[JsonPropertyName("paths")]
public List<string> Paths { get; set; } = new();
}