agent-workflow-kit-cli 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli/commands/add.js +1 -1
  2. package/dist/cli/commands/doctor.js +8 -1
  3. package/dist/cli/commands/init.js +91 -1
  4. package/dist/cli/index.js +2 -2
  5. package/dist/core/analyzer.js +146 -1
  6. package/dist/core/awos/intelligence.js +30 -6
  7. package/dist/core/detector.js +32 -4
  8. package/package.json +1 -1
  9. package/templates/dotnet/AGENTS.md.hbs +53 -0
  10. package/templates/dotnet/rules/api-structure.md +43 -0
  11. package/templates/dotnet/rules/csharp-style.md +33 -0
  12. package/templates/dotnet/rules/dependency-injection.md +54 -0
  13. package/templates/dotnet/rules/error-handling-validation.md +86 -0
  14. package/templates/dotnet/skills/dotnet-controller/SKILL.md +24 -0
  15. package/templates/express/AGENTS.md.hbs +41 -0
  16. package/templates/express/rules/error-handling.md +88 -0
  17. package/templates/express/rules/express-style.md +39 -0
  18. package/templates/express/rules/router-controller.md +27 -0
  19. package/templates/express/skills/express-endpoint/SKILL.md +24 -0
  20. package/templates/nestjs/AGENTS.md.hbs +38 -0
  21. package/templates/nestjs/rules/module-architecture.md +35 -0
  22. package/templates/nestjs/rules/nestjs-style.md +41 -0
  23. package/templates/nestjs/rules/validation-errors.md +46 -0
  24. package/templates/nestjs/skills/nestjs-module/SKILL.md +25 -0
  25. package/templates/next-js/AGENTS.md.hbs +41 -0
  26. package/templates/next-js/rules/data-fetching-mutations.md +36 -0
  27. package/templates/next-js/rules/next-style.md +32 -0
  28. package/templates/next-js/rules/seo-metadata.md +50 -0
  29. package/templates/next-js/rules/server-client-components.md +27 -0
  30. package/templates/next-js/skills/next-feature/SKILL.md +26 -0
  31. package/templates/react-ts/AGENTS.md.hbs +109 -32
  32. package/templates/react-ts/rules/data-fetching.md +45 -0
  33. package/templates/react-ts/rules/forms-validation.md +55 -0
  34. package/templates/react-ts/rules/premium-ui.md +45 -0
  35. package/templates/react-ts/rules/react-style.md +38 -26
  36. package/templates/react-ts/rules/routing-splitting.md +21 -0
  37. package/templates/react-ts/rules/seo-accessibility.md +34 -0
  38. package/templates/react-ts/skills/react-feature/SKILL.md +5 -5
@@ -11,7 +11,7 @@ import { analyzeModule } from "../../core/analyzer.js";
11
11
  import { updateGitignore } from "./init.js";
12
12
  export async function runAdd(stack, options) {
13
13
  const targetStack = stack.toLowerCase();
14
- const validStacks = ["spring-boot", "react-ts", "fastapi", "python-ai"];
14
+ const validStacks = ["spring-boot", "react-ts", "next-js", "nestjs", "express", "fastapi", "python-ai", "dotnet"];
15
15
  if (!validStacks.includes(targetStack)) {
16
16
  console.error(chalk.red(`Error: Invalid stack '${stack}'. Supported stacks are: ${validStacks.join(", ")}`));
17
17
  process.exit(1);
@@ -96,10 +96,17 @@ npx agent-workflow-kit-cli doctor || exit 1
96
96
  console.log(chalk.gray(`Running: ${cmd} ${args.join(" ")}`));
97
97
  await execa(cmd, args, { cwd, stdio: "inherit" });
98
98
  }
99
- else if (stack === "react-ts") {
99
+ else if (stack === "react-ts" ||
100
+ stack === "next-js" ||
101
+ stack === "nestjs" ||
102
+ stack === "express") {
100
103
  console.log(chalk.gray("Running: npx tsc --noEmit"));
101
104
  await execa("npx", ["tsc", "--noEmit"], { cwd, stdio: "inherit" });
102
105
  }
106
+ else if (stack === "dotnet") {
107
+ console.log(chalk.gray("Running: dotnet build"));
108
+ await execa("dotnet", ["build"], { cwd, stdio: "inherit" });
109
+ }
103
110
  console.log(chalk.green(`✔️ ${stack} validation passed!`));
104
111
  }
105
112
  catch (err) {
@@ -30,7 +30,14 @@ function printSuccessAndNextSteps(options) {
30
30
  }
31
31
  export async function updateGitignore(targetDir, dryRun) {
32
32
  const gitignorePath = path.join(targetDir, ".gitignore");
33
- const rulesToIgnore = [".cursorrules", ".copilot-instructions.md", ".clinerules"];
33
+ const rulesToIgnore = [
34
+ ".cursorrules",
35
+ ".copilot-instructions.md",
36
+ ".clinerules",
37
+ "AGENTS.md",
38
+ "GEMINI.md",
39
+ ".agents/"
40
+ ];
34
41
  if (dryRun) {
35
42
  console.log(chalk.gray(`[Dry Run] Would update .gitignore in ${targetDir} to exclude IDE rule files.`));
36
43
  return;
@@ -78,6 +85,87 @@ async function writeWorkspaceIdeRulesAndGitignore(cwd, options) {
78
85
  }
79
86
  await updateGitignore(cwd, options.dryRun);
80
87
  }
88
+ async function writeDefaultWorkflow(cwd, options) {
89
+ const workflowsDir = path.join(cwd, ".agents", "workflows");
90
+ const workflowFile = path.join(workflowsDir, "pr-verification.json");
91
+ const sampleWorkflow = {
92
+ id: "pr-verification",
93
+ name: "Pull Request Verification & ADR Flow",
94
+ description: "Compiles project, validates architecture boundaries, creates ADR, and requests human sign-off.",
95
+ version: "1.0.0",
96
+ supportedArchitectures: ["layered", "clean-architecture", "feature-first"],
97
+ requiredRoles: ["developer"],
98
+ graph: {
99
+ nodes: [
100
+ {
101
+ id: "build-and-lint",
102
+ name: "Compile and Lint Check",
103
+ type: "task",
104
+ executor: "command",
105
+ params: {
106
+ command: "npm run build || mvn compile || python -m py_compile **/*.py"
107
+ },
108
+ mockOutput: {
109
+ status: "success",
110
+ duration: "3.5s"
111
+ }
112
+ },
113
+ {
114
+ id: "verify-architecture",
115
+ name: "Verify Layer Boundaries",
116
+ type: "task",
117
+ executor: "command",
118
+ params: {
119
+ command: "npx agent-workflow-kit-cli profile"
120
+ },
121
+ mockOutput: {
122
+ violationsFound: 0,
123
+ status: "clean"
124
+ }
125
+ },
126
+ {
127
+ id: "create-adr",
128
+ name: "Generate Architecture Decision Record",
129
+ type: "task",
130
+ executor: "adr-generate",
131
+ params: {
132
+ title: "Automated PR Validation Record",
133
+ status: "proposed",
134
+ context: "Auto-generated during pipeline test run.",
135
+ decision: "All structural layers passed boundary checks successfully.",
136
+ consequences: "Workspace rules integrity confirmed."
137
+ },
138
+ mockOutput: {
139
+ adrId: "ADR-0001",
140
+ status: "proposed"
141
+ }
142
+ },
143
+ {
144
+ id: "operator-approval",
145
+ name: "Operator Sign-off Hook",
146
+ type: "approval"
147
+ }
148
+ ],
149
+ edges: [
150
+ { sourceId: "build-and-lint", targetId: "verify-architecture" },
151
+ { sourceId: "verify-architecture", targetId: "create-adr" },
152
+ { sourceId: "create-adr", targetId: "operator-approval" }
153
+ ]
154
+ }
155
+ };
156
+ if (options.dryRun) {
157
+ console.log(chalk.gray(`[Dry Run] Would create default workflow under ${workflowFile}`));
158
+ return;
159
+ }
160
+ try {
161
+ await fs.mkdir(workflowsDir, { recursive: true });
162
+ await fs.writeFile(workflowFile, JSON.stringify(sampleWorkflow, null, 2), "utf8");
163
+ console.log(chalk.green("✔️ Created default workflow pack: .agents/workflows/pr-verification.json"));
164
+ }
165
+ catch (err) {
166
+ console.warn(chalk.yellow(`Could not create default workflow: ${err instanceof Error ? err.message : String(err)}`));
167
+ }
168
+ }
81
169
  export async function runInit(options) {
82
170
  const cwd = process.cwd();
83
171
  console.log(chalk.bold.cyan("\n🚀 Agent Workflow Kit - Initializing..."));
@@ -292,5 +380,7 @@ export async function runInit(options) {
292
380
  }
293
381
  // Write workspace-level IDE rules and update gitignore
294
382
  await writeWorkspaceIdeRulesAndGitignore(cwd, options);
383
+ // Write default sample workflow DAG
384
+ await writeDefaultWorkflow(cwd, options);
295
385
  printSuccessAndNextSteps(options);
296
386
  }
package/dist/cli/index.js CHANGED
@@ -19,11 +19,11 @@ export function runCli() {
19
19
  program
20
20
  .name("agent-workflow-kit")
21
21
  .description("Generate AI coding workflows/rules/templates for Codex and Antigravity")
22
- .version("1.3.0");
22
+ .version("1.3.2");
23
23
  program
24
24
  .command("init")
25
25
  .description("Initialize agent guidelines and skills for the repository")
26
- .option("--stack <stack>", "Specify target stack: auto | spring-boot | react-ts | fastapi", "auto")
26
+ .option("--stack <stack>", "Specify target stack: auto | spring-boot | react-ts | next-js | nestjs | express | fastapi | dotnet", "auto")
27
27
  .option("--agent <agent>", "Specify target agent profile: both | codex | antigravity", "both")
28
28
  .option("--dry-run", "Output actions to console without writing any files", false)
29
29
  .action(async (options) => {
@@ -356,22 +356,36 @@ async function analyzeSpringBoot(dir) {
356
356
  async function analyzeReactTs(dir) {
357
357
  let packageManager = "npm";
358
358
  let runCommand = "npm run";
359
+ let executeCommand = "npx";
360
+ let lockFile = "package-lock.json";
359
361
  try {
360
362
  const hasYarnLock = await fs.stat(path.join(dir, "yarn.lock")).then((s) => s.isFile()).catch(() => false);
361
363
  const hasPnpmLock = await fs.stat(path.join(dir, "pnpm-lock.yaml")).then((s) => s.isFile()).catch(() => false);
364
+ const hasBunLockb = await fs.stat(path.join(dir, "bun.lockb")).then((s) => s.isFile()).catch(() => false);
365
+ const hasBunLock = await fs.stat(path.join(dir, "bun.lock")).then((s) => s.isFile()).catch(() => false);
362
366
  if (hasYarnLock) {
363
367
  packageManager = "yarn";
364
368
  runCommand = "yarn";
369
+ executeCommand = "yarn dlx";
370
+ lockFile = "yarn.lock";
365
371
  }
366
372
  else if (hasPnpmLock) {
367
373
  packageManager = "pnpm";
368
374
  runCommand = "pnpm";
375
+ executeCommand = "pnpm dlx";
376
+ lockFile = "pnpm-lock.yaml";
377
+ }
378
+ else if (hasBunLockb || hasBunLock) {
379
+ packageManager = "bun";
380
+ runCommand = "bun run";
381
+ executeCommand = "bunx";
382
+ lockFile = hasBunLockb ? "bun.lockb" : "bun.lock";
369
383
  }
370
384
  }
371
385
  catch {
372
386
  // Use default
373
387
  }
374
- return { packageManager, runCommand };
388
+ return { packageManager, runCommand, executeCommand, lockFile };
375
389
  }
376
390
  /**
377
391
  * Analyzes FastAPI project details.
@@ -439,6 +453,42 @@ async function analyzePythonAi(dir) {
439
453
  }
440
454
  return { packageManager, runCommand, hasGpuLibraries, hasHardwareLibraries };
441
455
  }
456
+ /**
457
+ * Analyzes Next.js project details.
458
+ */
459
+ async function analyzeNextJs(dir) {
460
+ const ctx = await analyzeReactTs(dir);
461
+ return {
462
+ packageManager: ctx.packageManager,
463
+ runCommand: ctx.runCommand,
464
+ executeCommand: ctx.executeCommand,
465
+ lockFile: ctx.lockFile,
466
+ };
467
+ }
468
+ /**
469
+ * Analyzes NestJS project details.
470
+ */
471
+ async function analyzeNestJs(dir) {
472
+ const ctx = await analyzeReactTs(dir);
473
+ return {
474
+ packageManager: ctx.packageManager,
475
+ runCommand: ctx.runCommand,
476
+ executeCommand: ctx.executeCommand,
477
+ lockFile: ctx.lockFile,
478
+ };
479
+ }
480
+ /**
481
+ * Analyzes Express.js project details.
482
+ */
483
+ async function analyzeExpress(dir) {
484
+ const ctx = await analyzeReactTs(dir);
485
+ return {
486
+ packageManager: ctx.packageManager,
487
+ runCommand: ctx.runCommand,
488
+ executeCommand: ctx.executeCommand,
489
+ lockFile: ctx.lockFile,
490
+ };
491
+ }
442
492
  /**
443
493
  * Entry point to analyze module configurations.
444
494
  */
@@ -451,12 +501,107 @@ export async function analyzeModule(dir, stacks) {
451
501
  else if (stack === "react-ts") {
452
502
  context["react-ts"] = await analyzeReactTs(dir);
453
503
  }
504
+ else if (stack === "next-js") {
505
+ context["next-js"] = await analyzeNextJs(dir);
506
+ }
507
+ else if (stack === "nestjs") {
508
+ context["nestjs"] = await analyzeNestJs(dir);
509
+ }
510
+ else if (stack === "express") {
511
+ context["express"] = await analyzeExpress(dir);
512
+ }
454
513
  else if (stack === "fastapi") {
455
514
  context["fastapi"] = await analyzeFastApi(dir);
456
515
  }
457
516
  else if (stack === "python-ai") {
458
517
  context["python-ai"] = await analyzePythonAi(dir);
459
518
  }
519
+ else if (stack === "dotnet") {
520
+ context["dotnet"] = await analyzeDotnet(dir);
521
+ }
460
522
  }
461
523
  return context;
462
524
  }
525
+ /**
526
+ * Analyzes dotnet project details.
527
+ */
528
+ async function analyzeDotnet(dir) {
529
+ let solutionName = path.basename(dir);
530
+ const projectNames = [];
531
+ const testProjects = [];
532
+ let dotnetVersion = "8.0";
533
+ try {
534
+ const entries = await fs.readdir(dir, { withFileTypes: true });
535
+ // Find .sln file
536
+ const slnFile = entries.find(e => e.isFile() && e.name.endsWith(".sln"));
537
+ if (slnFile) {
538
+ solutionName = path.parse(slnFile.name).name;
539
+ }
540
+ // Find all .csproj files (recursively)
541
+ const csprojFiles = await findCsprojFiles(dir);
542
+ for (const file of csprojFiles) {
543
+ const projName = path.parse(file).name;
544
+ try {
545
+ const content = await fs.readFile(file, "utf8");
546
+ if (content.includes("Microsoft.NET.Test.Sdk") || projName.toLowerCase().includes("test")) {
547
+ testProjects.push(projName);
548
+ }
549
+ else {
550
+ projectNames.push(projName);
551
+ }
552
+ // Try to parse TargetFramework
553
+ const match = content.match(/<TargetFramework>net([\d\.]+)<\/TargetFramework>/);
554
+ if (match && match[1]) {
555
+ dotnetVersion = match[1];
556
+ }
557
+ }
558
+ catch {
559
+ projectNames.push(projName);
560
+ }
561
+ }
562
+ }
563
+ catch {
564
+ // Keep defaults
565
+ }
566
+ return {
567
+ solutionName,
568
+ projectNames,
569
+ dotnetVersion,
570
+ testProjects,
571
+ };
572
+ }
573
+ async function findCsprojFiles(dir) {
574
+ const files = [];
575
+ async function traverse(currentDir) {
576
+ if (files.length >= 20)
577
+ return;
578
+ try {
579
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
580
+ for (const entry of entries) {
581
+ if (files.length >= 20)
582
+ return;
583
+ const fullPath = path.join(currentDir, entry.name);
584
+ if (entry.isDirectory()) {
585
+ if (entry.name.startsWith(".") ||
586
+ entry.name === "node_modules" ||
587
+ entry.name === "bin" ||
588
+ entry.name === "obj" ||
589
+ entry.name === "target" ||
590
+ entry.name === "build" ||
591
+ entry.name === "dist") {
592
+ continue;
593
+ }
594
+ await traverse(fullPath);
595
+ }
596
+ else if (entry.isFile() && entry.name.endsWith(".csproj")) {
597
+ files.push(fullPath);
598
+ }
599
+ }
600
+ }
601
+ catch {
602
+ // Ignore
603
+ }
604
+ }
605
+ await traverse(dir);
606
+ return files;
607
+ }
@@ -141,23 +141,41 @@ export async function buildRepositoryContext(workspaceRoot) {
141
141
  }
142
142
  // Determine default architecture style based on stack
143
143
  let architecture = "layered";
144
- if (mainStack === "spring-boot") {
144
+ if (mainStack === "spring-boot" || mainStack === "dotnet") {
145
145
  architecture = "clean-architecture";
146
146
  }
147
- else if (mainStack === "react-ts") {
147
+ else if (mainStack === "react-ts" || mainStack === "next-js") {
148
148
  architecture = "feature-first";
149
149
  }
150
+ else if (mainStack === "nestjs") {
151
+ architecture = "module-driven";
152
+ }
153
+ else if (mainStack === "express") {
154
+ architecture = "layered";
155
+ }
150
156
  else if (mainStack === "fastapi") {
151
157
  architecture = "vertical-slice";
152
158
  }
153
159
  // Testing strategy defaults
154
160
  const frameworks = [];
155
- if (mainStack === "react-ts")
161
+ if (mainStack === "react-ts") {
156
162
  frameworks.push("vitest", "testing-library");
157
- else if (mainStack === "spring-boot")
163
+ }
164
+ else if (mainStack === "next-js") {
165
+ frameworks.push("jest", "playwright");
166
+ }
167
+ else if (mainStack === "nestjs" || mainStack === "express") {
168
+ frameworks.push("jest", "supertest");
169
+ }
170
+ else if (mainStack === "spring-boot") {
158
171
  frameworks.push("junit", "mockito");
159
- else if (mainStack === "fastapi")
172
+ }
173
+ else if (mainStack === "fastapi") {
160
174
  frameworks.push("pytest");
175
+ }
176
+ else if (mainStack === "dotnet") {
177
+ frameworks.push("xunit", "moq");
178
+ }
161
179
  const testing = {
162
180
  frameworks,
163
181
  coverageGoal: 80,
@@ -167,12 +185,18 @@ export async function buildRepositoryContext(workspaceRoot) {
167
185
  if (mainStack === "spring-boot") {
168
186
  validationLibraries.push("jakarta.validation");
169
187
  }
170
- else if (mainStack === "react-ts") {
188
+ else if (mainStack === "react-ts" || mainStack === "next-js" || mainStack === "express") {
171
189
  validationLibraries.push("zod");
172
190
  }
191
+ else if (mainStack === "nestjs") {
192
+ validationLibraries.push("class-validator");
193
+ }
173
194
  else if (mainStack === "fastapi") {
174
195
  validationLibraries.push("pydantic");
175
196
  }
197
+ else if (mainStack === "dotnet") {
198
+ validationLibraries.push("fluentvalidation");
199
+ }
176
200
  const registry = await loadAWOSPlugins(workspaceRoot);
177
201
  let context = {
178
202
  stack: mainStack,
@@ -38,16 +38,30 @@ async function detectProjectStackDirect(cwd) {
38
38
  }
39
39
  }
40
40
  }
41
- // 2. Detect React + TypeScript (package.json containing react dependency)
41
+ // 2. Detect Node.js / JavaScript / TypeScript stacks (React, Next.js, NestJS, Express)
42
42
  try {
43
43
  const pkgPath = path.join(cwd, "package.json");
44
44
  const content = await fs.readFile(pkgPath, "utf8");
45
45
  const pkgJson = JSON.parse(content);
46
- const hasReact = (pkgJson.dependencies && pkgJson.dependencies.react) ||
47
- (pkgJson.devDependencies && pkgJson.devDependencies.react);
48
- if (hasReact) {
46
+ const deps = pkgJson.dependencies || {};
47
+ const devDeps = pkgJson.devDependencies || {};
48
+ const hasReact = deps.react || devDeps.react;
49
+ const hasNext = deps.next || devDeps.next;
50
+ const hasNest = deps["@nestjs/core"] || devDeps["@nestjs/core"];
51
+ const hasExpress = deps.express || devDeps.express;
52
+ if (hasNext) {
53
+ stacks.push("next-js");
54
+ }
55
+ else if (hasReact) {
49
56
  stacks.push("react-ts");
50
57
  }
58
+ const hasNestConfig = await fs.stat(path.join(cwd, "nest-cli.json")).then((s) => s.isFile()).catch(() => false);
59
+ if (hasNest || hasNestConfig) {
60
+ stacks.push("nestjs");
61
+ }
62
+ else if (hasExpress) {
63
+ stacks.push("express");
64
+ }
51
65
  }
52
66
  catch {
53
67
  // Ignore
@@ -102,6 +116,20 @@ async function detectProjectStackDirect(cwd) {
102
116
  }
103
117
  }
104
118
  }
119
+ // 4. Detect .NET (files ending in .csproj or .sln, or global.json)
120
+ try {
121
+ const entries = await fs.readdir(cwd, { withFileTypes: true });
122
+ const hasDotnet = entries.some((entry) => entry.isFile() &&
123
+ (entry.name.endsWith(".csproj") ||
124
+ entry.name.endsWith(".sln") ||
125
+ entry.name === "global.json"));
126
+ if (hasDotnet) {
127
+ stacks.push("dotnet");
128
+ }
129
+ }
130
+ catch {
131
+ // Ignore
132
+ }
105
133
  return stacks;
106
134
  }
107
135
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-workflow-kit-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "AI-Ready Repository Workflow Generator & Guideline Optimizer for Codex and Antigravity",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,53 @@
1
+ ## 🗺️ Hướng Dẫn Phát Triển .NET (C#)
2
+
3
+ Dự án này là một ứng dụng .NET (C#), được tổ chức và cấu hình như sau:
4
+ - **Tên Solution:** `{{solutionName}}`
5
+ - **Phiên bản .NET:** `net{{dotnetVersion}}`
6
+ - **Các dự án thành phần:**
7
+ {{#each projectNames}}
8
+ - `{{this}}`
9
+ {{/each}}
10
+ - **Các dự án kiểm thử (Test Projects):**
11
+ {{#each testProjects}}
12
+ - `{{this}}`
13
+ {{/each}}
14
+
15
+ ---
16
+
17
+ ### 🔄 Vòng Đời Phát Triển Tác Nhân (Agent Development Lifecycle)
18
+ Tác nhân AI phải thực hiện tất cả các nhiệm vụ .NET theo quy trình 5 bước sau:
19
+ 1. **Thiết kế & Lập trình:** Triển khai các lớp nghiệp vụ tuân thủ cấu trúc Clean Architecture / Onion Architecture. Đặt Core Logic trong Application/Domain layer độc lập.
20
+ 2. **Kiểm thử Toàn diện:** Viết unit test cho các Service sử dụng xUnit và Moq.
21
+ 3. **Chất lượng Mã nguồn:** Chạy các lệnh kiểm tra lỗi cú pháp và kiểu dữ liệu:
22
+ - Chạy Build kiểm tra: `dotnet build`
23
+ - Chạy Kiểm thử: `dotnet test`
24
+ 4. **Quản lý Thư viện:** Thêm các gói NuGet mới qua lệnh: `dotnet add package [tên_gói]`.
25
+
26
+ ---
27
+
28
+ ### 🏗️ Kiến Trúc Hệ Thống Bản Mẫu (Template Blueprint)
29
+ Vui lòng tham khảo các tài liệu quy tắc chi tiết dưới đây:
30
+ - Quy ước coding style chuẩn C#, PascalCase, camelCase và tiền tố Interface: `@csharp-style.md`
31
+ - Quy định đăng ký và sử dụng Dependency Injection (Transient, Scoped, Singleton): `@dependency-injection.md`
32
+ - Phân định trách nhiệm API Controller và điều phối luồng xử lý: `@api-structure.md`
33
+ - Cấu hình FluentValidation và middleware xử lý ngoại lệ tập trung: `@error-handling-validation.md`
34
+ - Quy trình từng bước sinh mới API Controller, DTO, Request Validator: `@SKILL.md`
35
+
36
+ ---
37
+
38
+ ### 🏛️ Quy Tắc Phát Triển Nghiêm Ngặt
39
+
40
+ #### 1. Kiến Trúc Sạch (Clean Architecture)
41
+ Phân tách rõ ràng các tầng trách nhiệm:
42
+ - **Domain:** Chứa Entities, Value Objects, Domain Logic. Tuyệt đối không phụ thuộc vào bất kỳ thư viện ngoài nào.
43
+ - **Application:** Chứa Interfaces, DTOs, Use Cases (Services/Queries/Commands). Chỉ phụ thuộc vào Domain layer.
44
+ - **Infrastructure:** Chứa Data Access (EF Core), External Services. Phụ thuộc vào Application layer.
45
+ - **Presentation (API):** Chứa Controllers, Middlewares. Điểm khởi chạy của ứng dụng.
46
+
47
+ #### 2. Đăng Ký Dependency Injection (DI)
48
+ - Sử dụng constructor injection để tiêm các dịch vụ cần thiết qua các interface.
49
+ - Tuyệt đối cấm sử dụng Service Locator pattern (ví dụ: giải quyết dependencies thủ công qua `IServiceProvider` trong runtime).
50
+
51
+ #### 3. Kiểm Thử Dữ Liệu Tự Động Với FluentValidation
52
+ - Toàn bộ dữ liệu đầu vào của API Request phải được kiểm duyệt tự động thông qua các class Validator thừa kế từ `AbstractValidator<T>` của thư viện FluentValidation.
53
+ - Không viết code validate thủ công bằng các câu lệnh `if` rải rác trong Controller.
@@ -0,0 +1,43 @@
1
+ # Cấu Trúc Lớp Presentation (API Controllers)
2
+
3
+ Tài liệu này quy định cấu trúc và trách nhiệm của API Controllers trong tầng Presentation của ứng dụng .NET.
4
+
5
+ ---
6
+
7
+ ## 🏛️ Trách Nhiệm Của API Controller
8
+ API Controller chịu trách nhiệm:
9
+ 1. Nhận yêu cầu HTTP (HTTP Requests) từ client.
10
+ 2. Ánh xạ các tham số URL, Query String, hoặc Request Body vào các DTO tương ứng.
11
+ 3. Chuyển giao các tham số đó sang tầng Application (Services/Queries/Commands).
12
+ 4. Nhận lại kết quả và phản hồi Client thông qua các mã trạng thái HTTP phù hợp (200 OK, 201 Created, 400 Bad Request, 404 Not Found, v.v.).
13
+
14
+ ---
15
+
16
+ ## 🚦 Quy Tắc Phát Triển
17
+ - **Controller mỏng (Thin Controllers):** Controllers không chứa bất kỳ logic nghiệp vụ, tính toán thuật toán, hay truy vấn DB trực tiếp. 100% logic đó phải nằm dưới Application layer.
18
+ - **Không tiêm DbContext vào Controller:** DbContext phải được che giấu dưới Infrastructure / Repository layer. Tầng Presentation tuyệt đối không gọi DbContext.
19
+ - **Sử dụng DTO thay vì Entity trực tiếp:**
20
+ - Không nhận trực tiếp Domain Entities làm đầu vào của Controller.
21
+ - Không trả trực tiếp Domain Entities về cho Client để tránh lộ cấu trúc DB hoặc lỗi cyclic references. Luôn ánh xạ qua các lớp DTO (Data Transfer Objects).
22
+ - **Cấu hình định tuyến Route rõ ràng:** Sử dụng attribute routing để khai báo rõ ràng phương thức HTTP và endpoint:
23
+ ```csharp
24
+ [ApiController]
25
+ [Route("api/v1/[controller]")]
26
+ public class CoursesController : ControllerBase
27
+ {
28
+ private readonly ICourseService _courseService;
29
+
30
+ public CoursesController(ICourseService courseService)
31
+ {
32
+ _courseService = courseService;
33
+ }
34
+
35
+ [HttpGet("{id}")]
36
+ public async Task<IActionResult> GetById(Guid id)
37
+ {
38
+ var course = await _courseService.GetByIdAsync(id);
39
+ if (course == null) return NotFound();
40
+ return Ok(course);
41
+ }
42
+ }
43
+ ```
@@ -0,0 +1,33 @@
1
+ # Quy Ước Viết Mã C# (C# Coding Style & Conventions)
2
+
3
+ Tài liệu này đặc tả quy chuẩn coding style, đặt tên biến, tên lớp và cách tổ chức file mã nguồn trong dự án .NET.
4
+
5
+ ---
6
+
7
+ ## 🏷️ Quy Ước Đặt Tên (Naming Conventions)
8
+
9
+ ### Quy tắc đặt tên Lớp và Thành phần
10
+ - **PascalCase:** Áp dụng cho Class, Struct, Record, Enum, Interface, Method, và Public Properties.
11
+ - *Ví dụ:* `UserService`, `GetActiveUsers()`, `CreatedDate`.
12
+ - **Interface Prefix:** Tên Interface bắt buộc phải bắt đầu bằng chữ cái `I`.
13
+ - *Ví dụ:* `IUserService`, `IUserRepository`.
14
+ - **camelCase:** Áp dụng cho tham số đầu vào của hàm (method arguments) và các biến cục bộ (local variables).
15
+ - *Ví dụ:* `userId`, `requestPayload`.
16
+ - **Private Fields Prefix:** Các trường dữ liệu private trong Class phải viết bằng camelCase và bắt đầu bằng dấu gạch dưới `_`.
17
+ - *Ví dụ:* `private readonly IUserRepository _userRepository;`.
18
+
19
+ ---
20
+
21
+ ## 📦 Tổ Chức Mã Nguồn & Định Dạng
22
+ - **Namespaces:** Đặt namespace phản ánh cấu trúc thư mục của dự án để đảm bảo tính dễ tìm kiếm.
23
+ - *Ví dụ:* `ProjectName.Application.Services`.
24
+ - **File Scoped Namespaces:** Sử dụng cú pháp namespace phạm vi file (File-scoped namespace) của C# 10+ để giảm cấp thụt lề thụt dòng không cần thiết:
25
+ ```csharp
26
+ namespace ProjectName.Application.Services;
27
+
28
+ public class UserService : IUserService
29
+ {
30
+ // ...
31
+ }
32
+ ```
33
+ - **Sắp xếp Usings:** Đặt các chỉ thị `using` hệ thống (`System.*`) lên trước, tiếp theo là các thư viện bên thứ ba, và cuối cùng là các dự án nội bộ.
@@ -0,0 +1,54 @@
1
+ # Đăng Ký & Sử Dụng Dependency Injection (DI)
2
+
3
+ Tài liệu này hướng dẫn cách cấu hình và tiêm phụ thuộc trong các ứng dụng .NET Core.
4
+
5
+ ---
6
+
7
+ ## 💉 Constructor Injection (Tiêm qua Hàm khởi tạo)
8
+ - **Quy tắc bắt buộc:** Luôn luôn giải quyết các phụ thuộc của Class thông qua Constructor. Gán chúng vào các trường private readonly có tiền tố gạch dưới `_`.
9
+ - Tuyệt đối không dùng Service Locator (không gọi `app.Services.GetService<T>()` hay tiêm `IServiceProvider` để lấy service động tại runtime).
10
+
11
+ * **Không hợp lệ (❌ Nghiêm cấm):**
12
+ ```csharp
13
+ public class OrderService
14
+ {
15
+ private readonly IServiceProvider _serviceProvider;
16
+
17
+ public OrderService(IServiceProvider serviceProvider)
18
+ {
19
+ _serviceProvider = serviceProvider;
20
+ }
21
+
22
+ public void Process()
23
+ {
24
+ var repo = _serviceProvider.GetService<IOrderRepository>();
25
+ // ...
26
+ }
27
+ }
28
+ ```
29
+ * **Hợp lệ (✔️ Khuyến khích):**
30
+ ```csharp
31
+ public class OrderService : IOrderService
32
+ {
33
+ private readonly IOrderRepository _orderRepository;
34
+
35
+ public OrderService(IOrderRepository orderRepository)
36
+ {
37
+ _orderRepository = orderRepository;
38
+ }
39
+
40
+ public void Process()
41
+ {
42
+ var orders = _orderRepository.GetAll();
43
+ // ...
44
+ }
45
+ }
46
+ ```
47
+
48
+ ---
49
+
50
+ ## ⚙️ Đăng Ký Vòng Đời Dịch Vụ (Service Lifetimes)
51
+ Lựa chọn đúng vòng đời khi đăng ký dịch vụ trong `Program.cs`:
52
+ 1. **Transient (`AddTransient<I, T>()`):** Tạo mới mỗi khi được yêu cầu. Dành cho các dịch vụ nhẹ, không lưu trạng thái (Stateless Services).
53
+ 2. **Scoped (`AddScoped<I, T>()`):** Tạo mới một lần cho mỗi HTTP Request. Thích hợp cho Repository, Database Context (`DbContext`), và các dịch vụ nghiệp vụ.
54
+ 3. **Singleton (`AddSingleton<I, T>()`):** Tạo một lần duy nhất cho toàn bộ vòng đời ứng dụng. Dành cho caching in-memory, cấu hình hệ thống, helper dùng chung.