??? ???? ???? ??? ?? ???? ???? ??? ? ?????? ?? ?? ??
- WPF/MVVM, ASP.NET API, React/Vue/Next, Node, Python API, Spring, Android, Go, Rust CLI, generic solution ??? ProjectScaffold? ???? ???? ????? ??? - Code ?? empty workspace ??? ??? ????? ??? ?? ??? file_write? ?? ???? ??? ????? file_manage/file_write? ?? ?? ???? ???? ??? - AgentLoop ?? ? ProjectLayoutGate? ??? ??? ?? ??? ?? ??? ??? ????? ?? ??? ? ????? ??? - code-scaffold.skill.md? when_to_use? file_manage/file_edit ?????? ??? proactive auto-skill ???? ??? - IntentGate, scaffold profile, code quality ?? ???? ??? ?? ?? - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_project_scaffold_layout\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout\\ ?? 0 / ?? 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "IntentGateServiceTests|ProjectScaffoldProfileCatalogTests|SkillServiceRuntimePolicyTests|AgentLoopCodeQualityTests" -p:OutputPath=bin\\verify_project_scaffold_layout_tests\\ -p:IntermediateOutputPath=obj\\verify_project_scaffold_layout_tests\\ ?? 183
This commit is contained in:
583
src/AxCopilot/Services/Agent/ProjectScaffoldProfileCatalog.cs
Normal file
583
src/AxCopilot/Services/Agent/ProjectScaffoldProfileCatalog.cs
Normal file
@@ -0,0 +1,583 @@
|
||||
using System.IO;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record ProjectScaffoldProfile(
|
||||
string Key,
|
||||
string Label,
|
||||
IReadOnlyList<string> TriggerTokens,
|
||||
IReadOnlyList<string> ExpectedDirectories,
|
||||
IReadOnlyList<string> StarterPaths,
|
||||
IReadOnlyList<string> AllowedRootFiles,
|
||||
int MinimumDirectoryHits = 2);
|
||||
|
||||
internal sealed record ProjectScaffoldLayoutAssessment(
|
||||
bool IsSatisfied,
|
||||
int MatchedDirectoryCount,
|
||||
IReadOnlyList<string> ExistingDirectories,
|
||||
IReadOnlyList<string> MissingDirectories,
|
||||
IReadOnlyList<string> SuspiciousRootFiles);
|
||||
|
||||
internal static class ProjectScaffoldProfileCatalog
|
||||
{
|
||||
private static readonly string[] s_creationVerbs =
|
||||
[
|
||||
"create",
|
||||
"generate",
|
||||
"build",
|
||||
"write",
|
||||
"scaffold",
|
||||
"bootstrap",
|
||||
"set up",
|
||||
"setup",
|
||||
"make",
|
||||
"implement",
|
||||
"start",
|
||||
"만들",
|
||||
"생성",
|
||||
"작성",
|
||||
"구현",
|
||||
"구축",
|
||||
"세팅",
|
||||
"스캐폴드"
|
||||
];
|
||||
|
||||
private static readonly string[] s_projectNouns =
|
||||
[
|
||||
"project",
|
||||
"app",
|
||||
"application",
|
||||
"service",
|
||||
"api",
|
||||
"solution",
|
||||
"starter",
|
||||
"template",
|
||||
"boilerplate",
|
||||
"workspace",
|
||||
"repo",
|
||||
"repository",
|
||||
"library",
|
||||
"package",
|
||||
"module",
|
||||
"프로젝트",
|
||||
"앱",
|
||||
"애플리케이션",
|
||||
"서비스",
|
||||
"솔루션",
|
||||
"템플릿",
|
||||
"보일러플레이트",
|
||||
"워크스페이스",
|
||||
"저장소",
|
||||
"라이브러리",
|
||||
"패키지",
|
||||
"모듈"
|
||||
];
|
||||
|
||||
private static readonly string[] s_languageTokens =
|
||||
[
|
||||
"c#",
|
||||
"dotnet",
|
||||
".net",
|
||||
"csproj",
|
||||
"wpf",
|
||||
"xaml",
|
||||
"mvvm",
|
||||
"viewmodel",
|
||||
"resourcedictionary",
|
||||
"resource dictionary",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"react",
|
||||
"next",
|
||||
"vue",
|
||||
"nuxt",
|
||||
"node",
|
||||
"express",
|
||||
"python",
|
||||
"fastapi",
|
||||
"flask",
|
||||
"django",
|
||||
"java",
|
||||
"spring",
|
||||
"spring boot",
|
||||
"kotlin",
|
||||
"android",
|
||||
"go",
|
||||
"golang",
|
||||
"rust",
|
||||
"cli",
|
||||
"web",
|
||||
"desktop",
|
||||
"mobile",
|
||||
"frontend",
|
||||
"backend",
|
||||
"api",
|
||||
"웹",
|
||||
"데스크톱",
|
||||
"모바일",
|
||||
"프론트엔드",
|
||||
"백엔드",
|
||||
"파이썬",
|
||||
"자바",
|
||||
"코틀린",
|
||||
"안드로이드",
|
||||
"리액트",
|
||||
"뷰",
|
||||
"고",
|
||||
"러스트",
|
||||
"스프링",
|
||||
"장고",
|
||||
"플라스크",
|
||||
"패스트api"
|
||||
];
|
||||
|
||||
private static readonly string[] s_solutionShapeTokens =
|
||||
[
|
||||
"frontend",
|
||||
"backend",
|
||||
"desktop",
|
||||
"mobile",
|
||||
"cli",
|
||||
"tool",
|
||||
"worker",
|
||||
"daemon",
|
||||
"game",
|
||||
"프론트엔드",
|
||||
"백엔드",
|
||||
"데스크톱",
|
||||
"모바일",
|
||||
"도구",
|
||||
"워커",
|
||||
"게임"
|
||||
];
|
||||
|
||||
private static readonly HashSet<string> s_commonAllowedRootFiles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"README.md",
|
||||
".gitignore",
|
||||
".editorconfig",
|
||||
".gitattributes",
|
||||
".env",
|
||||
".env.example",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
"tsconfig.json",
|
||||
"vite.config.ts",
|
||||
"vite.config.js",
|
||||
"next.config.js",
|
||||
"next.config.mjs",
|
||||
"nuxt.config.ts",
|
||||
"requirements.txt",
|
||||
"pyproject.toml",
|
||||
"poetry.lock",
|
||||
"Pipfile",
|
||||
"Pipfile.lock",
|
||||
"pom.xml",
|
||||
"build.gradle",
|
||||
"build.gradle.kts",
|
||||
"settings.gradle",
|
||||
"settings.gradle.kts",
|
||||
"Cargo.toml",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"composer.json",
|
||||
"Gemfile",
|
||||
"index.html"
|
||||
};
|
||||
|
||||
private static readonly string[] s_relevantRootExtensions =
|
||||
[
|
||||
".cs",
|
||||
".xaml",
|
||||
".razor",
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".css",
|
||||
".scss",
|
||||
".py",
|
||||
".java",
|
||||
".kt",
|
||||
".go",
|
||||
".rs",
|
||||
".php",
|
||||
".rb",
|
||||
".json",
|
||||
".xml",
|
||||
".yaml",
|
||||
".yml"
|
||||
];
|
||||
|
||||
private static readonly IReadOnlyList<ProjectScaffoldProfile> s_profiles =
|
||||
[
|
||||
new(
|
||||
Key: "wpf-mvvm",
|
||||
Label: "WPF / MVVM desktop app",
|
||||
TriggerTokens:
|
||||
[
|
||||
"wpf",
|
||||
"xaml",
|
||||
"mvvm",
|
||||
"viewmodel",
|
||||
"resourcedictionary",
|
||||
"resource dictionary",
|
||||
"csproj",
|
||||
"desktop app",
|
||||
"window",
|
||||
"usercontrol",
|
||||
"user control"
|
||||
],
|
||||
ExpectedDirectories: ["Views", "ViewModels", "Models", "Services", "Themes", "Assets"],
|
||||
StarterPaths:
|
||||
[
|
||||
"App.xaml",
|
||||
"App.xaml.cs",
|
||||
"Views/MainWindow.xaml",
|
||||
"Views/MainWindow.xaml.cs",
|
||||
"ViewModels/MainWindowViewModel.cs",
|
||||
"Themes/Colors.xaml"
|
||||
],
|
||||
AllowedRootFiles: ["App.xaml", "App.xaml.cs"],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "aspnet-api",
|
||||
Label: "ASP.NET / Web API service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"asp.net",
|
||||
"aspnet",
|
||||
"web api",
|
||||
"webapi",
|
||||
"minimal api",
|
||||
"mvc",
|
||||
"blazor",
|
||||
"razor",
|
||||
"controller",
|
||||
"swagger"
|
||||
],
|
||||
ExpectedDirectories: ["Controllers", "Services", "Models", "Contracts", "Data", "Properties"],
|
||||
StarterPaths:
|
||||
[
|
||||
"Program.cs",
|
||||
"Controllers/HealthController.cs",
|
||||
"Services/HealthService.cs",
|
||||
"Models/HealthStatus.cs",
|
||||
"appsettings.json"
|
||||
],
|
||||
AllowedRootFiles: ["Program.cs", "appsettings.json", "appsettings.Development.json"],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "react-web",
|
||||
Label: "React / Next / Vue frontend",
|
||||
TriggerTokens:
|
||||
[
|
||||
"react",
|
||||
"next",
|
||||
"next.js",
|
||||
"nextjs",
|
||||
"vue",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
"vite",
|
||||
"frontend",
|
||||
"front-end",
|
||||
"spa",
|
||||
"component"
|
||||
],
|
||||
ExpectedDirectories: ["src/components", "src/pages", "src/services", "src/styles", "public"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/App.tsx",
|
||||
"src/main.tsx",
|
||||
"src/components/AppShell.tsx",
|
||||
"src/pages/HomePage.tsx",
|
||||
"src/services/api.ts"
|
||||
],
|
||||
AllowedRootFiles: ["index.html"],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "node-service",
|
||||
Label: "Node / Express / Nest backend",
|
||||
TriggerTokens:
|
||||
[
|
||||
"node",
|
||||
"express",
|
||||
"nestjs",
|
||||
"nest",
|
||||
"koa",
|
||||
"backend",
|
||||
"back-end",
|
||||
"rest api",
|
||||
"server app",
|
||||
"middleware"
|
||||
],
|
||||
ExpectedDirectories: ["src/controllers", "src/routes", "src/services", "src/models", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/index.ts",
|
||||
"src/controllers/HealthController.ts",
|
||||
"src/routes/index.ts",
|
||||
"src/services/HealthService.ts",
|
||||
"tests/health.spec.ts"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "python-service",
|
||||
Label: "Python API service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"python",
|
||||
"fastapi",
|
||||
"flask",
|
||||
"django",
|
||||
"uvicorn",
|
||||
"backend api",
|
||||
"python api"
|
||||
],
|
||||
ExpectedDirectories: ["app/api", "app/models", "app/services", "app/core", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"app/main.py",
|
||||
"app/api/routes.py",
|
||||
"app/services/health_service.py",
|
||||
"app/models/health.py",
|
||||
"tests/test_health.py"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "spring-service",
|
||||
Label: "Java / Spring Boot service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"spring",
|
||||
"spring boot",
|
||||
"java api",
|
||||
"maven",
|
||||
"gradle",
|
||||
"jpa",
|
||||
"controller",
|
||||
"repository"
|
||||
],
|
||||
ExpectedDirectories: ["src/main/java", "src/main/resources", "src/test/java"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/main/java/com/example/Application.java",
|
||||
"src/main/java/com/example/controller/HealthController.java",
|
||||
"src/main/java/com/example/service/HealthService.java",
|
||||
"src/main/resources/application.yml",
|
||||
"src/test/java/com/example/ApplicationTests.java"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "android-kotlin",
|
||||
Label: "Android / Kotlin app",
|
||||
TriggerTokens:
|
||||
[
|
||||
"android",
|
||||
"kotlin",
|
||||
"jetpack compose",
|
||||
"compose",
|
||||
"xml layout",
|
||||
"activity",
|
||||
"fragment"
|
||||
],
|
||||
ExpectedDirectories: ["app/src/main/java", "app/src/main/res/layout", "app/src/main/res/values", "app/src/test"],
|
||||
StarterPaths:
|
||||
[
|
||||
"app/src/main/java/com/example/MainActivity.kt",
|
||||
"app/src/main/res/layout/activity_main.xml",
|
||||
"app/src/main/res/values/colors.xml",
|
||||
"app/src/test/java/com/example/MainActivityTest.kt"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "go-service",
|
||||
Label: "Go service",
|
||||
TriggerTokens:
|
||||
[
|
||||
"golang",
|
||||
"go api",
|
||||
"go service",
|
||||
"gin",
|
||||
"echo",
|
||||
"fiber",
|
||||
"handler"
|
||||
],
|
||||
ExpectedDirectories: ["cmd", "internal", "pkg", "api", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"cmd/server/main.go",
|
||||
"internal/handler/health.go",
|
||||
"internal/service/health.go",
|
||||
"pkg/config/config.go",
|
||||
"tests/health_test.go"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "rust-cli",
|
||||
Label: "Rust CLI / tool",
|
||||
TriggerTokens:
|
||||
[
|
||||
"rust",
|
||||
"cargo",
|
||||
"cli",
|
||||
"command line",
|
||||
"terminal tool",
|
||||
"binary",
|
||||
"subcommand"
|
||||
],
|
||||
ExpectedDirectories: ["src/bin", "src/commands", "src/domain", "tests"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/main.rs",
|
||||
"src/commands/root.rs",
|
||||
"src/domain/config.rs",
|
||||
"tests/cli_tests.rs"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2),
|
||||
new(
|
||||
Key: "generic-solution",
|
||||
Label: "Generic structured solution",
|
||||
TriggerTokens:
|
||||
[
|
||||
"project",
|
||||
"solution",
|
||||
"starter",
|
||||
"template",
|
||||
"boilerplate",
|
||||
"library",
|
||||
"module",
|
||||
"workspace"
|
||||
],
|
||||
ExpectedDirectories: ["src", "tests", "docs", "config"],
|
||||
StarterPaths:
|
||||
[
|
||||
"src/main.ext",
|
||||
"src/services/service.ext",
|
||||
"tests/main.spec.ext",
|
||||
"docs/README.md"
|
||||
],
|
||||
AllowedRootFiles: [],
|
||||
MinimumDirectoryHits: 2)
|
||||
];
|
||||
|
||||
internal static ProjectScaffoldProfile? Detect(string? userQuery, string? activeTab)
|
||||
{
|
||||
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userQuery))
|
||||
return null;
|
||||
|
||||
var query = userQuery.ToLowerInvariant();
|
||||
if (!ContainsAny(query, s_creationVerbs))
|
||||
return null;
|
||||
|
||||
ProjectScaffoldProfile? bestProfile = null;
|
||||
var bestScore = 0;
|
||||
foreach (var profile in s_profiles)
|
||||
{
|
||||
var score = profile.TriggerTokens.Count(token => query.Contains(token, StringComparison.OrdinalIgnoreCase));
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestProfile = profile;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestProfile != null && bestScore > 0)
|
||||
return bestProfile;
|
||||
|
||||
if (ContainsAny(query, s_projectNouns) && ContainsAny(query, s_languageTokens))
|
||||
return GetProfile("generic-solution");
|
||||
|
||||
if (ContainsAny(query, s_projectNouns) && ContainsAny(query, s_solutionShapeTokens))
|
||||
return GetProfile("generic-solution");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsStructuredProjectRequest(string? userQuery, string? activeTab)
|
||||
=> Detect(userQuery, activeTab) != null;
|
||||
|
||||
internal static string BuildDirectoryPreview(ProjectScaffoldProfile profile, int maxCount = 5)
|
||||
=> string.Join(", ", profile.ExpectedDirectories.Take(Math.Max(1, maxCount)));
|
||||
|
||||
internal static string BuildStarterPathPreview(ProjectScaffoldProfile profile, int maxCount = 5)
|
||||
=> string.Join(", ", profile.StarterPaths.Take(Math.Max(1, maxCount)).Select(path => $"'{path}'"));
|
||||
|
||||
internal static ProjectScaffoldLayoutAssessment AssessLayout(ProjectScaffoldProfile profile, string? workFolder)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder))
|
||||
{
|
||||
return new ProjectScaffoldLayoutAssessment(
|
||||
IsSatisfied: false,
|
||||
MatchedDirectoryCount: 0,
|
||||
ExistingDirectories: [],
|
||||
MissingDirectories: profile.ExpectedDirectories.ToList(),
|
||||
SuspiciousRootFiles: []);
|
||||
}
|
||||
|
||||
var existingDirectories = profile.ExpectedDirectories
|
||||
.Where(dir => Directory.Exists(Path.Combine(workFolder, NormalizeDirectory(dir))))
|
||||
.ToList();
|
||||
var missingDirectories = profile.ExpectedDirectories
|
||||
.Where(dir => !existingDirectories.Contains(dir, StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
var allowedRootFiles = new HashSet<string>(s_commonAllowedRootFiles, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var file in profile.AllowedRootFiles)
|
||||
allowedRootFiles.Add(file);
|
||||
|
||||
var suspiciousRootFiles = Directory.EnumerateFiles(workFolder)
|
||||
.Select(Path.GetFileName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Select(name => name!)
|
||||
.Where(name =>
|
||||
{
|
||||
if (allowedRootFiles.Contains(name))
|
||||
return false;
|
||||
|
||||
var extension = Path.GetExtension(name);
|
||||
return s_relevantRootExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
})
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var isSatisfied = existingDirectories.Count >= profile.MinimumDirectoryHits
|
||||
&& suspiciousRootFiles.Count == 0;
|
||||
|
||||
return new ProjectScaffoldLayoutAssessment(
|
||||
IsSatisfied: isSatisfied,
|
||||
MatchedDirectoryCount: existingDirectories.Count,
|
||||
ExistingDirectories: existingDirectories,
|
||||
MissingDirectories: missingDirectories,
|
||||
SuspiciousRootFiles: suspiciousRootFiles);
|
||||
}
|
||||
|
||||
private static ProjectScaffoldProfile GetProfile(string key)
|
||||
=> s_profiles.First(profile => string.Equals(profile.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static string NormalizeDirectory(string value)
|
||||
=> value.Replace('/', Path.DirectorySeparatorChar);
|
||||
|
||||
private static bool ContainsAny(string text, IEnumerable<string> tokens)
|
||||
{
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (text.Contains(token, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user