@educa-corp/sdd-framework 0.5.0 → 0.7.0

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 (243) hide show
  1. package/bin/build.js +113 -19
  2. package/bin/gate-trace.js +487 -0
  3. package/bin/index.js +445 -146
  4. package/bin/lint-trace.js +643 -0
  5. package/bin/self-check.js +804 -2
  6. package/bin/trace-schema.json +621 -10
  7. package/core/FRAMEWORK_VERSION +1 -1
  8. package/core/README.md +20 -0
  9. package/core/commands/amend-prd.md +518 -0
  10. package/core/commands/debug.md +123 -511
  11. package/core/commands/define-product.md +86 -510
  12. package/core/commands/dev-gen-test.md +86 -510
  13. package/core/commands/dev-run-test.md +133 -519
  14. package/core/commands/dev-smoke-test.md +86 -510
  15. package/core/commands/extend-prd.md +128 -522
  16. package/core/commands/fix-bug.md +118 -509
  17. package/core/commands/generate-architecture.md +94 -515
  18. package/core/commands/generate-bdd.md +128 -513
  19. package/core/commands/generate-code.md +119 -510
  20. package/core/commands/generate-design-spec.md +86 -510
  21. package/core/commands/generate-prd.md +89 -510
  22. package/core/commands/generate-spec-manifest.md +86 -510
  23. package/core/commands/generate-tech-docs.md +120 -512
  24. package/core/commands/learn.md +172 -496
  25. package/core/commands/map-testids.md +86 -510
  26. package/core/commands/propose-scenario.md +86 -510
  27. package/core/commands/qc-analyze.md +86 -510
  28. package/core/commands/qc-design-test.md +86 -510
  29. package/core/commands/qc-plan.md +86 -510
  30. package/core/commands/qc-report.md +86 -510
  31. package/core/commands/qc-review.md +86 -510
  32. package/core/commands/qc-run-test.md +115 -513
  33. package/core/commands/refine-prd.md +112 -522
  34. package/core/commands/report-bug.md +86 -510
  35. package/core/commands/review-code.md +123 -511
  36. package/core/commands/review-context.md +136 -522
  37. package/core/commands/review-tech-docs.md +90 -511
  38. package/core/commands/setup-ai-first.md +166 -138
  39. package/core/commands/sync.md +155 -107
  40. package/core/commands/update-framework.md +16 -103
  41. package/core/commands/validate-traces.md +426 -511
  42. package/core/hooks/data-guard.js +174 -83
  43. package/core/hooks/settings.json +2 -1
  44. package/core/rules/workflow.md +64 -4
  45. package/core/steps/capture-lesson.md +34 -1
  46. package/core/steps/context-loader.md +50 -8
  47. package/core/steps/gate.md +92 -35
  48. package/core/steps/report-footer.md +23 -0
  49. package/core/templates/README.md +24 -1
  50. package/core/templates/ci/trace-gate.yml +146 -0
  51. package/core/templates/feature.template +1 -1
  52. package/core/templates/hooks/pre-push +61 -0
  53. package/docs/02-concepts/architecture.md +61 -6
  54. package/docs/02-concepts/traceability.md +57 -0
  55. package/docs/03-guides/architect.md +63 -0
  56. package/docs/04-reference/commands.md +148 -134
  57. package/docs/04-reference/model-selection.md +32 -19
  58. package/docs/04-reference/trace-schema.md +39 -0
  59. package/docs/explain/02b-extend-prd.md +1 -1
  60. package/docs/explain/02c-amend-prd.md +152 -0
  61. package/docs/explain/21-validate-traces.md +2 -1
  62. package/docs/explain/27-learn.md +5 -3
  63. package/docs/explain/28-sync.md +25 -0
  64. package/docs/explain/README.md +136 -135
  65. package/package.json +5 -9
  66. package/commands/debug.md +0 -917
  67. package/commands/debug.tmpl +0 -257
  68. package/commands/define-product.md +0 -862
  69. package/commands/define-product.tmpl +0 -225
  70. package/commands/dev-gen-test.md +0 -1124
  71. package/commands/dev-gen-test.tmpl +0 -490
  72. package/commands/dev-run-test.md +0 -859
  73. package/commands/dev-run-test.tmpl +0 -225
  74. package/commands/dev-smoke-test.md +0 -798
  75. package/commands/dev-smoke-test.tmpl +0 -217
  76. package/commands/extend-prd.md +0 -907
  77. package/commands/extend-prd.tmpl +0 -270
  78. package/commands/fix-bug.md +0 -910
  79. package/commands/fix-bug.tmpl +0 -197
  80. package/commands/generate-architecture.md +0 -775
  81. package/commands/generate-architecture.tmpl +0 -194
  82. package/commands/generate-bdd.md +0 -1347
  83. package/commands/generate-bdd.tmpl +0 -590
  84. package/commands/generate-code.md +0 -1283
  85. package/commands/generate-code.tmpl +0 -649
  86. package/commands/generate-design-spec.md +0 -1161
  87. package/commands/generate-design-spec.tmpl +0 -524
  88. package/commands/generate-prd.md +0 -1143
  89. package/commands/generate-prd.tmpl +0 -223
  90. package/commands/generate-spec-manifest.md +0 -745
  91. package/commands/generate-spec-manifest.tmpl +0 -164
  92. package/commands/generate-tech-docs.md +0 -1344
  93. package/commands/generate-tech-docs.tmpl +0 -273
  94. package/commands/learn.md +0 -723
  95. package/commands/learn.tmpl +0 -63
  96. package/commands/map-testids.md +0 -662
  97. package/commands/map-testids.tmpl +0 -81
  98. package/commands/propose-scenario.md +0 -783
  99. package/commands/propose-scenario.tmpl +0 -202
  100. package/commands/qc-analyze.md +0 -693
  101. package/commands/qc-analyze.tmpl +0 -112
  102. package/commands/qc-design-test.md +0 -650
  103. package/commands/qc-design-test.tmpl +0 -69
  104. package/commands/qc-plan.md +0 -630
  105. package/commands/qc-plan.tmpl +0 -49
  106. package/commands/qc-report.md +0 -641
  107. package/commands/qc-report.tmpl +0 -60
  108. package/commands/qc-review.md +0 -634
  109. package/commands/qc-review.tmpl +0 -53
  110. package/commands/qc-run-test.md +0 -750
  111. package/commands/qc-run-test.tmpl +0 -116
  112. package/commands/refine-prd.md +0 -1074
  113. package/commands/refine-prd.tmpl +0 -278
  114. package/commands/report-bug.md +0 -729
  115. package/commands/report-bug.tmpl +0 -148
  116. package/commands/review-code.md +0 -803
  117. package/commands/review-code.tmpl +0 -143
  118. package/commands/review-context.md +0 -1323
  119. package/commands/review-context.tmpl +0 -527
  120. package/commands/review-tech-docs.md +0 -982
  121. package/commands/review-tech-docs.tmpl +0 -401
  122. package/commands/setup-ai-first.md +0 -574
  123. package/commands/setup-ai-first.tmpl +0 -378
  124. package/commands/sync.md +0 -486
  125. package/commands/sync.tmpl +0 -384
  126. package/commands/update-framework.md +0 -290
  127. package/commands/update-framework.tmpl +0 -188
  128. package/commands/validate-traces.md +0 -1435
  129. package/commands/validate-traces.tmpl +0 -854
  130. package/hooks/data-guard.js +0 -141
  131. package/hooks/settings.json +0 -18
  132. package/modules/android-compose/module.yaml +0 -13
  133. package/modules/android-compose/stack-profile.yaml +0 -57
  134. package/modules/angular/architecture-snippets/component-patterns.md +0 -187
  135. package/modules/angular/module.yaml +0 -6
  136. package/modules/angular/stack-profile.yaml +0 -38
  137. package/modules/context-engineering/architecture-snippets/context-design.md +0 -119
  138. package/modules/context-engineering/module.yaml +0 -9
  139. package/modules/context-engineering/stack-profile.yaml +0 -61
  140. package/modules/dotnet/architecture-snippets/clean-arch.md +0 -160
  141. package/modules/dotnet/module.yaml +0 -6
  142. package/modules/dotnet/stack-profile.yaml +0 -50
  143. package/modules/flutter/module.yaml +0 -14
  144. package/modules/flutter/stack-profile.yaml +0 -59
  145. package/modules/golang/architecture-snippets/domain-layout.md +0 -283
  146. package/modules/golang/module.yaml +0 -6
  147. package/modules/golang/stack-profile.yaml +0 -40
  148. package/modules/ios-swiftui/module.yaml +0 -13
  149. package/modules/ios-swiftui/stack-profile.yaml +0 -55
  150. package/modules/java-spring/architecture-snippets/layered-arch.md +0 -201
  151. package/modules/java-spring/module.yaml +0 -15
  152. package/modules/java-spring/stack-profile.yaml +0 -28
  153. package/modules/nextjs/architecture-snippets/app-router-patterns.md +0 -269
  154. package/modules/nextjs/module.yaml +0 -14
  155. package/modules/nextjs/stack-profile.yaml +0 -74
  156. package/modules/nuxt/module.yaml +0 -14
  157. package/modules/nuxt/stack-profile.yaml +0 -58
  158. package/modules/phaser-game/architecture-snippets/phaser-scene-patterns.md +0 -646
  159. package/modules/phaser-game/module.yaml +0 -15
  160. package/modules/phaser-game/stack-profile.yaml +0 -90
  161. package/modules/php-laravel/architecture-snippets/service-repository.md +0 -302
  162. package/modules/php-laravel/module.yaml +0 -15
  163. package/modules/php-laravel/stack-profile.yaml +0 -56
  164. package/modules/qc-playwright/stack-profile.yaml +0 -66
  165. package/modules/react/architecture-snippets/hooks-query-patterns.md +0 -254
  166. package/modules/react/module.yaml +0 -14
  167. package/modules/react/stack-profile.yaml +0 -63
  168. package/modules/react-native/module.yaml +0 -14
  169. package/modules/react-native/stack-profile.yaml +0 -56
  170. package/modules/vue/module.yaml +0 -14
  171. package/modules/vue/stack-profile.yaml +0 -65
  172. package/rules/data-protection.md +0 -80
  173. package/rules/workflow.md +0 -73
  174. package/scripts/init.sh +0 -49
  175. package/scripts/upgrade.sh +0 -94
  176. package/skills/code/SKILL.md +0 -19
  177. package/skills/code/SKILL.tmpl +0 -19
  178. package/skills/debug/SKILL.md +0 -19
  179. package/skills/debug/SKILL.tmpl +0 -19
  180. package/skills/design-spec/SKILL.md +0 -11
  181. package/skills/design-spec/SKILL.tmpl +0 -11
  182. package/skills/discovery/SKILL.md +0 -14
  183. package/skills/discovery/SKILL.tmpl +0 -14
  184. package/skills/prd/SKILL.md +0 -19
  185. package/skills/prd/SKILL.tmpl +0 -19
  186. package/skills/qc/qa-analyst/DOC_GAPS.template.md +0 -63
  187. package/skills/qc/qa-analyst/acceptance-criteria.md +0 -60
  188. package/skills/qc/qa-analyst/business-rules.md +0 -59
  189. package/skills/qc/qa-analyst/data-flow.md +0 -64
  190. package/skills/qc/qa-analyst/spec-breakdown.md +0 -61
  191. package/skills/qc/qa-designer/e2e/journey.md +0 -41
  192. package/skills/qc/qa-designer/exploratory/charter.md +0 -68
  193. package/skills/qc/qa-designer/exploratory/explore-to-functional.md +0 -43
  194. package/skills/qc/qa-designer/functional/api.md +0 -45
  195. package/skills/qc/qa-designer/functional/gui-feature.md +0 -46
  196. package/skills/qc/qa-designer/functional/gui-screen.md +0 -52
  197. package/skills/qc/qa-designer/integration/api.md +0 -42
  198. package/skills/qc/qa-designer/integration/db.md +0 -39
  199. package/skills/qc/qa-designer/integration/gui.md +0 -40
  200. package/skills/qc/qa-designer/integration/kafka.md +0 -40
  201. package/skills/qc/qa-designer/non-functional.md +0 -40
  202. package/skills/qc/qa-planner/test-plan.md +0 -120
  203. package/skills/qc/qa-reviewer/script/e2e.md +0 -87
  204. package/skills/qc/qa-reviewer/script/exploratory.md +0 -45
  205. package/skills/qc/qa-reviewer/script/functional.md +0 -101
  206. package/skills/qc/qa-reviewer/script/integration.md +0 -91
  207. package/skills/qc/qa-reviewer/script/non-functional.md +0 -126
  208. package/skills/qc/qa-reviewer/test-case/e2e.md +0 -73
  209. package/skills/qc/qa-reviewer/test-case/exploratory.md +0 -43
  210. package/skills/qc/qa-reviewer/test-case/functional.md +0 -76
  211. package/skills/qc/qa-reviewer/test-case/integration.md +0 -69
  212. package/skills/qc/qa-reviewer/test-case/non-functional.md +0 -73
  213. package/skills/qc/qa-runner/e2e.md +0 -49
  214. package/skills/qc/qa-runner/exploratory/session.md +0 -36
  215. package/skills/qc/qa-runner/functional/api.md +0 -35
  216. package/skills/qc/qa-runner/functional/gui-feature.md +0 -51
  217. package/skills/qc/qa-runner/functional/gui-screen.md +0 -55
  218. package/skills/qc/qa-runner/integration.md +0 -47
  219. package/skills/qc/qa-runner/non-functional.md +0 -49
  220. package/skills/qc/qa-runner/report/report.md +0 -37
  221. package/skills/setup-ai-first/SKILL.md +0 -19
  222. package/skills/setup-ai-first/SKILL.tmpl +0 -19
  223. package/skills/spec/SKILL.md +0 -19
  224. package/skills/spec/SKILL.tmpl +0 -19
  225. package/skills/test/SKILL.md +0 -18
  226. package/skills/test/SKILL.tmpl +0 -18
  227. package/steps/business-language.md +0 -56
  228. package/steps/capture-lesson.md +0 -79
  229. package/steps/context-loader.md +0 -385
  230. package/steps/gate.md +0 -94
  231. package/steps/report-footer.md +0 -102
  232. package/steps/review-fanout.md +0 -159
  233. package/steps/spawn-agent.md +0 -129
  234. package/steps/trace-mirror.md +0 -53
  235. package/templates/README.md +0 -47
  236. package/templates/architecture.template.md +0 -394
  237. package/templates/design-spec.template.md +0 -217
  238. package/templates/feature.template +0 -123
  239. package/templates/platform-guide.template.md +0 -145
  240. package/templates/prd.template.md +0 -283
  241. package/templates/product-definition.template.md +0 -188
  242. package/templates/project-context.yaml +0 -212
  243. package/templates/tech-design.template.md +0 -490
@@ -2,28 +2,90 @@
2
2
  /**
3
3
  * data-guard.js — Claude Code PreToolUse Hook
4
4
  *
5
- * Blocks AI from reading, writing, or executing commands involving
6
- * sensitive files (credentials, secrets, private keys, .env, etc.)
5
+ * Chặn AI đọc/ghi/thực thi trên file nhạy cảm (credential, secret, private key, .env…).
7
6
  *
8
- * Install: copy to your project and register in .claude/settings.json
9
- * (see hooks/settings.json for registration template)
7
+ * Cài: copy vào project + đăng trong .claude/settings.json
8
+ * (mẫu: hooks/settings.json hoặc dùng `npx @educa-corp/sdd-framework --hooks`)
10
9
  *
11
- * Exit codes:
12
- * 0 = allow the tool call
13
- * 2 = block the tool call (Claude Code interprets this as a hard block)
10
+ * Exit code:
11
+ * 0 = cho phép
12
+ * 2 = chặn cứng (Claude Code hiểu này block)
13
+ *
14
+ * ── VÌ SAO BẢN NÀY KHÁC BẢN ĐẦU (GAPS-v3 G42) ────────────────────────────────
15
+ *
16
+ * Bản đầu có ba lỗi độc lập; hai trong số đó chặn đúng công việc mà framework tồn
17
+ * tại để làm, và cái còn lại làm lời hứa bảo mật không đúng:
18
+ *
19
+ * (a) matcher thiếu `Grep` — `Grep` với output_mode:"content" trên `.env` TRẢ VỀ
20
+ * nội dung file và không đi qua hook. Nên câu "AI will be blocked from reading
21
+ * .env" là sai.
22
+ * (b) pattern soi CẢ PATH, không neo. `/password/i` khớp
23
+ * `specs/auth/forgot-password/UC1.feature` và `src/auth/PasswordResetController.java`
24
+ * ⇒ một framework sinh code từ spec KHÔNG LÀM NỔI feature auth khi bật hook
25
+ * của chính nó.
26
+ * (c) Bash soi cả command string ⇒ `git commit -m "feat: password reset UC1"` bị
27
+ * chặn — đúng câu commit mà các lệnh hướng dẫn chạy ở cuối pipeline.
28
+ *
29
+ * Cả hai đường đều dẫn tới cùng một kết cục: người dùng TẮT HOOK, và mất luôn phần
30
+ * bảo vệ thật. Một guard bị tắt bảo vệ bằng không.
31
+ *
32
+ * ── MÔ HÌNH MỚI ──────────────────────────────────────────────────────────────
33
+ *
34
+ * Secret sống trong file CẤU HÌNH/DỮ LIỆU, không sống trong file MÃ NGUỒN.
35
+ * `secrets.json` là kho bí mật; `PasswordResetController.java` là code.
36
+ * Nên pattern chung (secret/credential/password/token/api-key) chỉ áp cho file
37
+ * KHÔNG phải mã nguồn. Pattern cứng (.env, .pem, .key…) áp cho mọi file.
38
+ * Cộng thêm allowlist thư mục làm việc của framework (specs/, .trace/, docs/…).
14
39
  */
15
40
 
16
- const readline = require('readline');
17
-
18
- // ── Sensitive file patterns ────────────────────────────────────────────────
41
+ // ── Thư mục làm việc của framework — không bao giờ chứa secret thật ───────────
42
+ // Tên feature ("forgot-password", "api-key-rotation") hay khớp pattern chung, và
43
+ // đây chính chỗ ca (b) nổ. Cho qua TRƯỚC khi soi.
44
+ const SAFE_DIR_PREFIXES = [
45
+ 'specs/', '.trace/', '.trace-mirror/', '.living-docs/',
46
+ 'docs/', '.agent/', 'feedback/', '.claude/',
47
+ ];
19
48
 
20
- const SENSITIVE_PATH_PATTERNS = [
21
- // Environment files
49
+ // ── Đuôi file MÃ NGUỒN — pattern chung không áp ───────────────────────────────
50
+ // Một file .java/.ts/.dart tên gì đi nữa cũng là code, không phải kho secret.
51
+ // (Pattern CỨNG bên dưới vẫn áp — nhưng .pem/.key không nằm trong danh sách này.)
52
+ const CODE_EXT = new Set([
53
+ 'java', 'kt', 'kts', 'scala', 'groovy',
54
+ 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'vue', 'svelte',
55
+ 'py', 'go', 'rs', 'rb', 'php', 'cs', 'fs', 'vb',
56
+ 'swift', 'm', 'mm', 'dart', 'c', 'h', 'cpp', 'hpp', 'cc',
57
+ 'sql', 'graphql', 'proto', 'feature', 'md', 'mdx', 'txt',
58
+ 'html', 'css', 'scss', 'less', 'tsv', 'csv',
59
+ ]);
60
+
61
+ // ── Pattern CỨNG — áp cho mọi file, soi TÊN FILE ─────────────────────────────
62
+ const SENSITIVE_NAME_HARD = [
22
63
  /^\.env$/i,
23
64
  /^\.env\./i,
24
65
  /\.env$/i,
25
66
 
26
- // Secret/credential files
67
+ /\.key$/i,
68
+ /\.pem$/i,
69
+ /\.p12$/i,
70
+ /\.pfx$/i,
71
+ /\.jks$/i,
72
+ /\.keystore$/i,
73
+ /^id_rsa$/i,
74
+ /^id_ed25519$/i,
75
+
76
+ /^application-(prod|production|staging)\.(yml|yaml|properties)$/i,
77
+ /^appsettings\.(Production|Staging)\.json$/i,
78
+ /^database\.yml$/i,
79
+ /^master\.key$/i,
80
+ /^oauth-private\.key$/i,
81
+ /^credentials$/i,
82
+ /^\.npmrc$/i,
83
+ /^\.pypirc$/i,
84
+ /^\.netrc$/i,
85
+ ];
86
+
87
+ // ── Pattern CHUNG — chỉ áp cho file KHÔNG phải mã nguồn, soi TÊN FILE ────────
88
+ const SENSITIVE_NAME_SOFT = [
27
89
  /secret/i,
28
90
  /credential/i,
29
91
  /password/i,
@@ -32,60 +94,84 @@ const SENSITIVE_PATH_PATTERNS = [
32
94
  /api[_-]?key/i,
33
95
  /access[_-]?token/i,
34
96
  /auth[_-]?token/i,
97
+ ];
35
98
 
36
- // Crypto keys and certificates
37
- /\.key$/i,
38
- /\.pem$/i,
39
- /\.p12$/i,
40
- /\.pfx$/i,
41
- /\.jks$/i,
42
- /\.keystore$/i,
43
-
44
- // Framework-specific prod configs
45
- /application-(prod|production|staging)\.(yml|yaml|properties)$/i,
46
- /appsettings\.(Production|Staging)\.json$/i,
47
- /database\.yml$/i,
48
- /config\/master\.key$/i,
49
- /storage\/oauth-private\.key$/i,
50
-
51
- // Secret directories
52
- /^secrets\//i,
53
- /\/secrets\//i,
54
- /^\.secrets\//i,
99
+ // ── Pattern THƯ MỤC — soi cả path ────────────────────────────────────────────
100
+ const SENSITIVE_DIR = [
101
+ /(^|\/)secrets?\//i,
102
+ /(^|\/)\.secrets?\//i,
103
+ /(^|\/)\.ssh\//i,
104
+ /(^|\/)\.aws\//i,
105
+ /(^|\/)\.gnupg\//i,
55
106
  ];
56
107
 
57
- const SENSITIVE_BASH_PATTERNS = [
108
+ // ── Lệnh bash nguy hiểm bất kể path ──────────────────────────────────────────
109
+ const SENSITIVE_BASH = [
58
110
  /\bprintenv\b/i,
59
- /\benv\b.*\|\s*(grep|awk|sed).*secret/i,
60
- /cat\s+\.env/i,
61
- /docker\s+inspect/i,
62
- /kubectl\s+get\s+secret.*-o\s+yaml/i,
111
+ /\benv\b\s*(\||$)/i,
112
+ /kubectl\s+get\s+secrets?\b[^|]*-o\s*(yaml|json)/i,
113
+ /\baws\s+secretsmanager\s+get-secret-value/i,
114
+ /\bgcloud\s+secrets\s+versions\s+access/i,
115
+ /\bvault\s+(read|kv\s+get)\b/i,
116
+ // `docker inspect` KHÔNG chặn cả cụm (lệnh debug thường dùng) — chỉ chặn khi nó
117
+ // thực sự moi Env ra. Bản đầu chặn cả cụm và đó là ma sát không đổi lấy gì.
118
+ /docker\s+inspect[^|]*\.Config\.Env/i,
63
119
  ];
64
120
 
65
- // ── Helpers ────────────────────────────────────────────────────────────────
121
+ // ── Helpers ──────────────────────────────────────────────────────────────────
122
+
123
+ function normalize(p) {
124
+ return String(p).replace(/\\/g, '/').replace(/^\.\//, '');
125
+ }
126
+
127
+ function extOf(base) {
128
+ const i = base.lastIndexOf('.');
129
+ return i > 0 ? base.slice(i + 1).toLowerCase() : '';
130
+ }
66
131
 
67
132
  function isSensitivePath(filePath) {
68
133
  if (!filePath) return false;
69
- const normalized = filePath.replace(/\\/g, '/');
70
- return SENSITIVE_PATH_PATTERNS.some(pattern => pattern.test(normalized));
134
+ const p = normalize(filePath);
135
+
136
+ // Vùng làm việc của framework — spec/trace/doc không chứa secret thật, và tên
137
+ // feature ở đó hay khớp pattern chung. Bỏ cả tiền tố tuyệt đối trước khi so.
138
+ const relish = p.replace(/^([a-zA-Z]:)?\/+/, '');
139
+ if (SAFE_DIR_PREFIXES.some(d => relish.startsWith(d) || relish.includes('/' + d))) return false;
140
+
141
+ if (SENSITIVE_DIR.some(re => re.test(p))) return true;
142
+
143
+ const base = p.split('/').pop() || '';
144
+ if (SENSITIVE_NAME_HARD.some(re => re.test(base))) return true;
145
+
146
+ // Pattern chung: bỏ qua mã nguồn. Đây là chỗ bản đầu chặn PasswordResetController.java.
147
+ if (CODE_EXT.has(extOf(base))) return false;
148
+ return SENSITIVE_NAME_SOFT.some(re => re.test(base));
71
149
  }
72
150
 
73
- function isSensitiveCommand(command) {
74
- if (!command) return false;
75
- return SENSITIVE_BASH_PATTERNS.some(pattern => pattern.test(command));
151
+ /**
152
+ * Tách các token TRÔNG GIỐNG PATH ra khỏi một command line.
153
+ * Bản đầu soi cả chuỗi lệnh nên `git commit -m "feat: password reset"` bị chặn.
154
+ */
155
+ function pathishTokens(command) {
156
+ return String(command)
157
+ .split(/[\s;|&()<>]+/)
158
+ .map(t => t.replace(/^["']|["']$/g, ''))
159
+ .filter(t => t && !t.startsWith('-') && /[\/.]/.test(t) && !/^-{1,2}/.test(t));
76
160
  }
77
161
 
78
- function block(reason) {
79
- console.error(`\n🔒 DATA GUARD — BLOCKED\n${reason}\n`);
80
- console.error('If you need configuration values, describe what you need');
81
- console.error('without sharing actual secrets. Use placeholder values in generated code.\n');
162
+ function block(reason, detail) {
163
+ console.error(`\n🔒 DATA GUARD — BLOCKED\n${reason}`);
164
+ if (detail) console.error(detail);
165
+ console.error('\nCần giá trị cấu hình? tả thứ bạn cần mà KHÔNG đưa secret thật;');
166
+ console.error('dùng placeholder trong code sinh ra.');
167
+ console.error('Nghĩ đây là chặn nhầm? Vùng specs/ .trace/ docs/ .agent/ luôn được cho qua,');
168
+ console.error('và file mã nguồn (.java/.ts/.dart…) không bị pattern chung đụng tới.\n');
82
169
  process.exit(2);
83
170
  }
84
171
 
85
- // ── Main ───────────────────────────────────────────────────────────────────
172
+ // ── Main ─────────────────────────────────────────────────────────────────────
86
173
 
87
174
  let rawInput = '';
88
-
89
175
  process.stdin.setEncoding('utf8');
90
176
  process.stdin.on('data', chunk => { rawInput += chunk; });
91
177
  process.stdin.on('end', () => {
@@ -93,49 +179,54 @@ process.stdin.on('end', () => {
93
179
  try {
94
180
  input = JSON.parse(rawInput);
95
181
  } catch {
96
- // Cannot parse input allow (fail open, not fail closed)
182
+ // Fail-open CÓ CHỦ Ý: hook parse lỗi chặn hết thì làm cả project đứng máy.
183
+ // Nhưng không im lặng — im lặng là cách một guard chết mà không ai biết.
184
+ console.error('⚠️ data-guard: không parse được input hook — CHO QUA (fail-open).');
97
185
  process.exit(0);
98
186
  }
99
187
 
100
- const toolName = input.tool_name || '';
101
- const toolInput = input.tool_input || {};
102
-
103
- // ── Read tool ─────────────────────────────────────────────────────────
104
- if (toolName === 'Read') {
105
- const filePath = toolInput.file_path || '';
106
- if (isSensitivePath(filePath)) {
107
- block(`Attempted to READ sensitive file: ${filePath}`);
108
- }
109
- }
110
-
111
- // ── Write tool ────────────────────────────────────────────────────────
112
- if (toolName === 'Write') {
113
- const filePath = toolInput.file_path || '';
114
- if (isSensitivePath(filePath)) {
115
- block(`Attempted to WRITE to sensitive file: ${filePath}`);
188
+ const tool = input.tool_name || '';
189
+ const arg = input.tool_input || {};
190
+
191
+ switch (tool) {
192
+ case 'Read':
193
+ case 'Write':
194
+ case 'Edit':
195
+ case 'NotebookEdit': {
196
+ const f = arg.file_path || arg.notebook_path || '';
197
+ if (isSensitivePath(f)) block(`Thao tác ${tool} trên file nhạy cảm: ${f}`);
198
+ break;
116
199
  }
117
- }
118
200
 
119
- // ── Edit tool ─────────────────────────────────────────────────────────
120
- if (toolName === 'Edit') {
121
- const filePath = toolInput.file_path || '';
122
- if (isSensitivePath(filePath)) {
123
- block(`Attempted to EDIT sensitive file: ${filePath}`);
201
+ // Grep ĐỌC ĐƯỢC NỘI DUNG (output_mode: "content"). Thiếu nhánh này thì mọi thứ
202
+ // trên chỉ là cửa trước, còn cửa sau mở toang — chính là lỗi (a) của G42.
203
+ case 'Grep': {
204
+ const target = arg.path || '';
205
+ if (target && isSensitivePath(target)) {
206
+ block(`Grep nhắm vào path nhạy cảm: ${target}`);
207
+ }
208
+ if (arg.glob && isSensitivePath(String(arg.glob).replace(/\*/g, 'x'))) {
209
+ block(`Grep dùng glob nhắm vào file nhạy cảm: ${arg.glob}`);
210
+ }
211
+ // Đang đi TÌM chính chuỗi secret — chặn kèm lý do khác hẳn.
212
+ if (arg.pattern && /\b(BEGIN [A-Z ]*PRIVATE KEY|aws_secret_access_key|sk-[A-Za-z0-9]{16,})/i
213
+ .test(String(arg.pattern))) {
214
+ block(`Grep đang tìm chính giá trị bí mật: ${String(arg.pattern).slice(0, 40)}…`);
215
+ }
216
+ break;
124
217
  }
125
- }
126
218
 
127
- // ── Bash tool ─────────────────────────────────────────────────────────
128
- if (toolName === 'Bash') {
129
- const command = toolInput.command || '';
130
- if (isSensitiveCommand(command)) {
131
- block(`Attempted to execute sensitive command: ${command}`);
132
- }
133
- // Also check if the command references a sensitive file path
134
- if (isSensitivePath(command)) {
135
- block(`Bash command references a sensitive file path: ${command}`);
219
+ case 'Bash': {
220
+ const cmd = arg.command || '';
221
+ if (SENSITIVE_BASH.some(re => re.test(cmd))) {
222
+ block(`Lệnh moi secret: ${cmd.slice(0, 120)}`);
223
+ }
224
+ // CHỈ soi token trông giống path — không soi cả chuỗi lệnh (lỗi (c) của G42).
225
+ const hit = pathishTokens(cmd).find(isSensitivePath);
226
+ if (hit) block(`Lệnh bash chạm file nhạy cảm: ${hit}`, ` (trong: ${cmd.slice(0, 120)})`);
227
+ break;
136
228
  }
137
229
  }
138
230
 
139
- // Allow all other tool calls
140
231
  process.exit(0);
141
232
  });
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "_comment": "Claude Code hook registration template. Copy this content into your project's .claude/settings.json",
3
+ "_matcher_note": "Grep BẮT BUỘC có trong matcher: Grep với output_mode:content ĐỌC ĐƯỢC nội dung file, nên thiếu nó thì việc chặn Read chỉ là cửa trước (GAPS-v3 G42a).",
3
4
  "_docs": "https://docs.anthropic.com/claude/claude-code/hooks",
4
5
 
5
6
  "hooks": {
6
7
  "PreToolUse": [
7
8
  {
8
- "matcher": "Read|Write|Edit|Bash",
9
+ "matcher": "Read|Write|Edit|Grep|NotebookEdit|Bash",
9
10
  "hooks": [
10
11
  {
11
12
  "type": "command",
@@ -7,10 +7,26 @@
7
7
 
8
8
  ## Checkpoints
9
9
 
10
- - **Always** show a CHECKPOINT before making significant changes.
11
- - A CHECKPOINT must include: what will be done, which files will be created/modified, estimated scope.
12
- - Wait for explicit "Y" or user confirmation before proceeding.
13
- - Exception: read-only analysis commands (`/review-code`, `/validate-traces`, `/debug`) may skip CHECKPOINT.
10
+ Ba mức, định nghĩa đầy đủ `steps/gate.md` Bước 3a — **đây chỉ là bản tóm tắt, gate là nguồn**:
11
+
12
+ | Mức | Lệnh nào | `--yes` bỏ qua? |
13
+ |---|---|:---:|
14
+ | **Không chặn** | read-only (`/review-code` · `/validate-traces` · `/debug` · `/review-context` · `/review-tech-docs`) | — |
15
+ | **Chặn thường** | mọi lệnh sinh/sửa artifact | ✅ |
16
+ | **Chặn CỨNG** | ghi đè file đã có · `--resume` · migrate · prune | ❌ |
17
+
18
+ - CHECKPOINT phải nêu **target đã phân giải**, và **mọi cờ 🔴/⚠️ mà context-loader đã tính**
19
+ (`active_service = unresolved`, `Status ≠ FULL`, CLAUDE.md thiếu, target resolve bằng wildcard).
20
+ - **KHÔNG lặp lại** những gì `[CTX LOADED]` vừa in ngay phía trên. Sạch hết thì CHECKPOINT
21
+ chỉ hai dòng.
22
+ - `--yes` bỏ qua *chặn thường*, **không** bỏ qua *chặn cứng*, và **không** tắt việc in cờ.
23
+
24
+ > **Vì sao ba mức thay vì "always show" (G41):** bản cũ viết *"**Always** show a CHECKPOINT"*
25
+ > rồi ngay dòng sau lại cấp một ngoại lệ cho lệnh read-only — mà `gate.md` **không hề thực thi**
26
+ > ngoại lệ đó. Hai file cùng được nạp vào mọi lệnh và nói ngược nhau. Cộng thêm: cổng luôn in
27
+ > ra một bảng giống hệt nhau, 20 lần cho một feature, nên `Y` thành phản xạ và cổng hỏng **âm
28
+ > thầm** — vẫn hiện, vẫn được trả lời, chỉ là không ai đọc. Cổng chỉ ồn khi thật sự có chuyện
29
+ > thì mới được đọc.
14
30
 
15
31
  ## Scope Control
16
32
 
@@ -20,9 +36,19 @@
20
36
 
21
37
  ## Trace Contract
22
38
 
39
+ > **Phạm vi:** mục này áp cho **repo framework**. Ở project consumer, `.agent/` là mirror sinh
40
+ > ra và `bin/` không được cài — contract ở đó là **read-only**: thấy lệch thì **báo**, đừng tự
41
+ > sửa (xem `.agent/README.md`). Việc duy nhất chạy được ở project là **kiểm sổ trace**:
42
+ > `npx @educa-corp/sdd-framework --lint-trace`.
43
+
23
44
  - Contract trace (field `@trace.*`, cột `.tsv`, path pattern, giá trị enum) có **một
24
45
  nguồn-sự-thật máy đọc**: `bin/trace-schema.json`. Bản cho người đọc:
25
46
  `docs/04-reference/trace-schema.md` — giữ hai file đồng bộ.
47
+ - **Canh contract ≠ canh dữ liệu.** `bin/self-check.js` đọc **file lệnh** và kiểm *"lệnh có gọi
48
+ đúng tên cột không"* — nó không bao giờ mở một `.tsv` thật. `bin/lint-trace.js` mở sổ thật.
49
+ Cần cả hai: sổ 24 cột được ghi **bằng tay**, hàng chục lần mỗi feature; một dấu tab thiếu ở
50
+ ô 17 dồn mọi ô sau đó sang trái, ô 21 `status` nhận một ngày tháng, và **không cờ nào bật**.
51
+ Thêm cột/vocabulary mới → khai binding cho `lint-trace` **ngay**; R8 fail build nếu quên.
26
52
  - Đổi contract (thêm/bỏ/đổi nghĩa một field, path, hay giá trị enum) → **sửa
27
53
  `bin/trace-schema.json` TRƯỚC**, rồi mới sửa lệnh. `npm run build` chạy
28
54
  `bin/self-check.js` và **fail** nếu lệnh lệch schema.
@@ -39,6 +65,40 @@
39
65
  **Ngoại lệ có chủ ý:** `qc_owner`/`qc_blocked_by` (con trỏ tới bug — spec đổi không làm bug biến
40
66
  mất) và `test_count`/`test_classes` (test vẫn tồn tại trên đĩa; số lượng không sai, chỉ nội dung
41
67
  cũ → **cảnh báo**, không hạ số, để tỷ lệ coverage không nhảy loạn).
68
+ - **Sửa spec phải đi qua một lệnh.** Mọi drift detector so **nhãn version**, không so **nội dung**
69
+ (0 content hash trong toàn bộ codebase) — nên một PRD/tech-doc bị sửa tay mà không bump version là
70
+ điểm mù **tuyệt đối**: cả `/validate-traces`, `gate-trace`, và `require-fresh-audit` đều xanh, và
71
+ cả ba **đúng theo định nghĩa của chính chúng**. Bốn cửa chính: `/generate-prd` (mới) ·
72
+ `/extend-prd` (**thêm**) · **`/amend-prd`** (**đổi**) · `/refine-prd`/`/review-context --resume`
73
+ (áp finding). `/validate-traces` Step 3.9 canh cửa sau bằng cờ 🔴 `PRD_UNTRACKED_EDIT`
74
+ (`spec_edit_detection`: git diff **và** git status vs mốc `spec_baseline`).
75
+ *Đường ra cố ý **tự lành**, không có `--accept-edit`: bump version + ghi row changelog nêu UC là
76
+ hết cờ. Một cờ escape sẽ là một đường dán nhãn lên thay đổi chưa ai xem — đúng cái ba rào của
77
+ `--realign` tồn tại để chặn.*
78
+ - **Làm mất hiệu lực có MỆNH ĐỀ ĐỐI NGẪU: ai KHẲNG ĐỊNH một giá trị dương phải được phép khẳng
79
+ định.** Luật ngay trên nói *"ai làm giá trị hết đúng thì phải hạ nó"* — đúng, và được thực thi tốt.
80
+ Nhưng thiếu nửa này thì chuỗi thành **hạ xuống → dựng lại**: `/generate-bdd` hạ
81
+ `dev_selftest → not_run` khi spec đổi, rồi `/dev-run-test` (lệnh kế tiếp trong vòng lặp dev bình
82
+ thường) ghi lại `pass` kèm **ngày hôm nay** vì test cũ + code cũ vẫn xanh.
83
+ `pass` **không** mang nghĩa *"test đã chạy xanh"* — nó mang nghĩa *"scenario này đã được nghiệm thu
84
+ theo spec **hiện tại**"*. Trên row `DRIFT` nghĩa thứ nhất đúng và nghĩa thứ hai **sai**. Nên `status`
85
+ trực giao với **kết quả chạy**, **KHÔNG** trực giao với **quyền khẳng định**.
86
+ Contract: `bin/trace-schema.json` → `positive_assertion_guards`; `self-check` **R14** canh chủ cột
87
+ thực sự rẽ nhánh theo `status`, `lint-trace` **T12** bắt trạng thái ở sổ thật bất kể ai ghi.
88
+ **`fail` không bao giờ bị chặn** — đây là guard chống *báo cáo sai*, không phải guard *che tin xấu*.
89
+ - **Dòng changelog là contract máy đọc, không phải ghi chú cho người đọc.** PRD và tech-doc gộp
90
+ đều phủ nhiều UC nhưng chỉ có **một** nhãn version, nên `/validate-traces` Step 4/5 lọc 🟠 `*_DRIFT`
91
+ vs ⓘ `*_STALE_REF` **bằng chính dòng đó**. Grammar khai ở `bin/trace-schema.json` →
92
+ `changelog_row_contract`; `self-check` **R12** fail build nếu lệch. Ba luật:
93
+ **(1)** mỗi mệnh đề mở đầu bằng **đơn vị sở hữu** — `{UC-ID}:` hoặc `PRD-global:`/`doc-global:`;
94
+ **(2)** BR/AC **luôn đi kèm UC sở hữu** (`UC3: sửa BR8`), **không bao giờ đứng một mình** —
95
+ consumer khớp theo UC, nên `sửa BR8` trơ trọi làm UC3 bị xếp ⓘ trong khi BR8 vừa đổi hành vi, và
96
+ `--realign-prd-version` (chỉ chặn 🟠) sẽ dán nhãn version lại lên đó;
97
+ **(3)** hậu tố `[no-behavior]` **chỉ** cho thay đổi mà producer **chứng minh được** là không đổi
98
+ hành vi (`changelog_row_contract.neutral_checks`) — không dành cho người tự khai.
99
+ *Lưới an toàn "row mơ hồ → 🟠 cho MỌI UC" đúng khi **thiếu** thông tin, và sai khi producer **có**
100
+ thông tin mà không ghi: đó là G52 — `/review-context --fix` từng ghi cứng một dòng 0 scope trong
101
+ khi findings YAML của nó có `uc_id` bắt buộc cho từng finding.*
42
102
  - **Mỗi audit flag phải quan sát được ở CẢ BA tầng.** Mọi giá trị trong
43
103
  `vocabularies.audit_flags` bắt buộc có đủ: **(1)** một counter `{flag_lowercase}_count`
44
104
  trong Step 7 + `summary` của `trace-report.json` · **(2)** một mảng trong `issues` ·
@@ -47,7 +47,7 @@ Nếu `lessons_path` chưa tồn tại, tạo file với header sau trước:
47
47
  > Các lỗi AI KHÔNG được lặp lại trong dự án này. Được nạp bởi context-loader ở đầu
48
48
  > mỗi lệnh và coi như ràng buộc cứng (cùng mức ưu tiên với coding standards trong CLAUDE.md).
49
49
  > Thêm bằng /learn, hoặc chấp nhận prompt trong /review-code, /fix-bug, /debug.
50
- > Commit file này để cả team dùng chung guardrail.
50
+ > Rà lại định kỳ bằng `/learn --review`. Commit file này để cả team dùng chung guardrail.
51
51
 
52
52
  | Category | Áp dụng cho |
53
53
  |----------|-----------|
@@ -58,6 +58,9 @@ Nếu `lessons_path` chưa tồn tại, tạo file với header sau trước:
58
58
  | prd | output của /generate-prd, /refine-prd |
59
59
  | general | mọi lệnh |
60
60
 
61
+ **Status:** `active` = đang là ràng buộc cứng · `retired` = đã hết đúng, GIỮ LẠI làm lịch sử
62
+ nhưng context-loader **không nạp nữa**. Lesson không ghi `Status` được coi là `active`.
63
+
61
64
  ---
62
65
  ```
63
66
 
@@ -65,6 +68,7 @@ Chèn lesson mới ngay dưới dấu phân cách `---` (**mới nhất lên đ
65
68
 
66
69
  ```markdown
67
70
  ### L-{NNN} — [{category}] {title}
71
+ - **Status**: active
68
72
  - **Date**: {hôm nay YYYY-MM-DD}
69
73
  - **Scope**: {scope}
70
74
  - **Mistake**: {mistake}
@@ -73,6 +77,35 @@ Chèn lesson mới ngay dưới dấu phân cách `---` (**mới nhất lên đ
73
77
 
74
78
  ```
75
79
 
80
+ ### Retire — đường ra của một lesson *(GAPS-v3 G46)*
81
+
82
+ Một lesson là **giả thuyết rằng AI sẽ lặp lại một lỗi**. Giả thuyết đó hết đúng khi code nó canh
83
+ không còn tồn tại, hoặc khi quy ước dự án đã đổi. Lúc đó nó phải bị **hạ xuống**, không được giữ.
84
+
85
+ Retire = đổi `Status` và ghi lý do — **KHÔNG xoá dòng**:
86
+
87
+ ```markdown
88
+ ### L-003 — [code-gen] Dùng WebClient thay RestTemplate
89
+ - **Status**: retired
90
+ - **Retired**: 2027-03-15 — project đổi tầng HTTP, RestTemplate không còn trong repo
91
+ - **Date**: 2026-08-19
92
+ …giữ nguyên phần còn lại…
93
+ ```
94
+
95
+ Giữ lại vì nó là **lịch sử**: người sau đọc được *"vì sao dự án này từng có luật đó"*, thứ mà xoá
96
+ đi là mất vĩnh viễn.
97
+
98
+ > **Vì sao cần đường ra:** trước G46 file này **chỉ có đường vào**. Lesson được nạp làm *"ràng buộc
99
+ > cứng, cùng mức ưu tiên với CLAUDE.md"* — vĩnh viễn, kể cả khi code nó canh đã bị xoá. Không cờ
100
+ > nào nhắc, không lệnh nào gỡ; cách duy nhất là có người tự nhớ ra rồi xoá tay.
101
+ > Đây đúng lớp lỗi của **G28** (*"giữ một `pass` đã hết hiệu lực là báo cáo sai"*) và luật
102
+ > `rules/workflow.md` §Trace Contract — **"làm mất hiệu lực ≠ ghi đè"** — chỉ là ở hàng đợi này
103
+ > chưa ai áp luật đó.
104
+ >
105
+ > **Ba trong bốn lệnh ghi lesson là lệnh phản ứng khi có sự cố** (`/review-code`, `/fix-bug`,
106
+ > `/debug`), nên file phình nhanh nhất đúng lúc dự án đang trục trặc — và lesson sinh ra lúc đó
107
+ > hay gắn với một sự cố cụ thể hơn là một quy tắc bền.
108
+
76
109
  ## L5 — Xác nhận
77
110
 
78
111
  In: `📝 Đã ghi lesson {id} → {lessons_path} ([{category}] {title})`
@@ -161,11 +161,25 @@ services:
161
161
 
162
162
  *(Cả 2a/2b/2c: override `paths.specs_dir`/`paths.tech_docs_dir` per-service CHỈ khi `setup.spec_source` KHÔNG được đặt. Khi `spec_source` ĐƯỢC đặt, MỌI BDD/tech-doc là artifact liên team → để bước 4 route sang spec repo; KHÔNG pin per-service ở đây.)*
163
163
 
164
- **3. Fallback**:
165
- - Không phát hiện được domain, hoặc domain không khớp key nào trong `services` → giữ path mặc định từ Bước 1, đặt `active_service = unresolved`.
166
- - Domain khớp một map-theo-platform (2b) nhưng `active_platform` xác định mà thiếu sub-key tương ứng → `active_service = unresolved`, ghi do để lệnh DỪNG báo lỗi cấu hình (không tự đoán platform).
167
- - Entry là map-theo-prd_slug (2c) nhưng `prd_slug` xác định thiếu key tương ứng `active_service = unresolved`, ghi do rõ (không tự đoán submodule).
168
- - Entry sai cấu trúc (vừa có `path` vừa có `by_prd_slug`, hoặc `by_prd_slug` lồng nhau) → `active_service = unresolved`, nêu đúng key sai để người dùng sửa `project-context.yaml`.
164
+ **3. Fallback** — **hai trạng thái khác nhau, đừng gộp** *(G51)*:
165
+
166
+ > `unrouted` = **chưa ai quyết** repo. Hợp lệ, bình thường feature đầu tiên của domain mới.
167
+ > `unresolved` = **config sai cấu trúc**. **bug** cần sửa file, không phải trạng thái chờ.
168
+ >
169
+ > Trước G51 cả hai dùng chung tên `unresolved` nên chịu chung hình phạt: `/generate-bdd` DỪNG HẲN.
170
+ > Nhưng PRD/BDD là artifact **nghiệp vụ** — PO biết `domain` và biết `platform`, **không** biết code
171
+ > sẽ nằm repo nào, và thường lúc đó chưa ai quyết. Cổng đặt sai phase.
172
+
173
+ **→ `unrouted`** (chưa có mapping — **không** phải lỗi):
174
+ - Không phát hiện được domain, hoặc domain **không khớp key nào** trong `services` → giữ path mặc định từ Bước 1, đặt `active_service = unrouted`.
175
+ - Domain khớp map-theo-platform (2b) nhưng thiếu sub-key cho `active_platform` → `active_service = unrouted`, ghi lý do rõ (không tự đoán platform).
176
+ - Entry là map-theo-prd_slug (2c) nhưng thiếu key cho `prd_slug` → `active_service = unrouted`, ghi lý do rõ (không tự đoán submodule).
177
+
178
+ **→ `unresolved`** (config **sai cấu trúc** — bug):
179
+ - Entry vừa có `path` vừa có `by_prd_slug`, hoặc `by_prd_slug` lồng nhau → `active_service = unresolved`, nêu đúng key sai để người dùng sửa `project-context.yaml`.
180
+
181
+ *Cả hai đều KHÔNG chặn việc nạp context. Lệnh nào chặn là quyết định của lệnh đó: `/generate-bdd`
182
+ đi tiếp với `unrouted` (Step 1.6) · `/generate-code` DỪNG ở cả hai (nó buộc phải biết ghi vào đâu).*
169
183
 
170
184
  **4. Tự động override theo spec source** — nếu `setup.spec_source` được đặt VÀ path tương ứng chưa được set tường minh trong `paths:`:
171
185
  - Override `paths.specs_dir` → `{spec_source}/specs` — **luôn khi `spec_source` được đặt.** Mọi spec artifact (PRD, BDD, tech-docs, design-spec) nằm dưới gốc spec thống nhất trong spec repo dùng chung theo bố cục feature-package: `{spec_source}/specs/{domain}/{prd-slug}/`. Mọi umbrella (FE/App/BE) đều đọc từ đây. *(`specs/` theo service chỉ khi không có `spec_source`.)*
@@ -209,6 +223,13 @@ Khi `active_service` đã được phân giải thành một path thật ở Bư
209
223
 
210
224
  **4. Nếu không tìm thấy config của service** — giữ mặc định umbrella, vẫn set `service_root = {active_service}` (luôn cần mốc path kể cả khi không có config override).
211
225
 
226
+ > ⚠️ **`service_root` KHÔNG BAO GIỜ được là một chuỗi trạng thái** *(G51)*. Nếu `active_service` là
227
+ > `unrouted` / `unresolved` / `multi` / `—` thì đặt **`service_root = null`** và giữ path mặc định
228
+ > umbrella — **đừng** nội suy giá trị đó thành tên thư mục.
229
+ > Bản trước đặt `service_root = {active_service}` vô điều kiện, nên `/generate-code` (ghi file
230
+ > **tương đối với `service_root`**) sẽ ghi source vào một thư mục tên đúng chữ `unresolved/`.
231
+ > `service_root = null` là tín hiệu để `/generate-code` DỪNG thay vì ghi bừa.
232
+
212
233
  ---
213
234
 
214
235
  ## Bước 2 — [PROJECT-CONFIG] Nạp module stack profile (có điều kiện)
@@ -334,13 +355,33 @@ Phân giải path file lessons:
334
355
  - Else mặc định `specs/domain-knowledge/lessons-learned.md`
335
356
  - Ở chế độ umbrella/service (khi `service_root` được set), nếu `paths.lessons_file` chưa set, mặc định `{service_root}/.agent/project-lessons.md`
336
357
 
337
- Nếu file tồn tại, đọc lưu TẤT CẢ lesson làm **GUARDRAIL ĐANG HOẠT ĐỘNG** cho phiên:
358
+ Nếu file tồn tại, **LỌC TRƯỚC KHI NẠP** chỉ giữ lesson thoả **cả hai**:
359
+
360
+ 1. **`Status: active`** (hoặc **không có** field `Status` → lesson cũ, coi là `active`)
361
+ 2. **`category` khớp lệnh đang chạy**, hoặc `category: general`
362
+
363
+ Số còn lại mới nạp làm **GUARDRAIL ĐANG HOẠT ĐỘNG** cho phiên:
338
364
  - Coi **Rule** của mỗi lesson là ràng buộc cứng — cùng mức ưu tiên với coding standards trong CLAUDE.md (Bước 3).
339
- - Trước khi sinh hoặc sửa bất kỳ artifact nào (PRD, BDD, tech-doc, code, test), đối chiếu output với mọi lesson `category` khớp lệnh hiện tại VÀ `scope` khớp target (domain / file).
365
+ - Trước khi sinh hoặc sửa bất kỳ artifact nào (PRD, BDD, tech-doc, code, test), đối chiếu output với lesson đã nạp **`scope` khớp target** (domain / file glob).
340
366
  - Nếu output sinh ra vi phạm một lesson → sửa **trước khi** trình bày, và ghi rõ lesson nào (`L-NNN`) đã được áp dụng.
341
367
 
368
+ Ghi lại **hai** con số cho recap Bước 7: `{n_active_for_this_command}` và `{n_total_active}`.
369
+
342
370
  Nếu file không tồn tại → bỏ qua âm thầm (chưa có lesson nào được ghi nhận).
343
371
 
372
+ > **Vì sao lọc ở ĐÂY chứ không phải lúc dùng (GAPS-v3 G46):** bản cũ viết *"đọc và lưu **TẤT CẢ**
373
+ > lesson"* ở dòng trên, rồi *"đối chiếu với mọi lesson có `category` khớp"* ở dòng dưới. Bộ lọc
374
+ > **đã tồn tại** — chỉ là chạy **sau** khi đã nạp hết. Có 6 category, nên `/generate-prd` đang nạp
375
+ > cả đống lesson `code-gen` mà nó không bao giờ dùng tới. Chuyển bộ lọc lên trước là thay đổi thứ
376
+ > tự, không phải thêm logic.
377
+ >
378
+ > **Và vì sao chỉ nạp `active`:** trước G46 file lessons **chỉ có đường vào**. Một lesson viết năm
379
+ > ngoái cho code đã bị xoá vẫn được nạp làm ràng buộc cứng, mãi mãi. Cùng lớp lỗi với G28 — giữ
380
+ > một tín hiệu đã hết đúng. Đường ra: `/learn --review` (xem `capture-lesson.md` §Retire).
381
+ >
382
+ > ⚠️ **Không bao giờ tự bỏ lesson vì file quá dài.** Nạp thiếu một guardrail trong im lặng đúng là
383
+ > thứ framework này tồn tại để chống. Vượt ngưỡng thì **cảnh báo** ở recap, người quyết retire.
384
+
344
385
  ---
345
386
 
346
387
  ## Bước 7 — [RECAP] Working Memory Recap (chống lost-in-middle)
@@ -360,7 +401,8 @@ CLAUDE.md : {root + {service_root} | chỉ {service_root} | chỉ root | ⚠️
360
401
  Ticket : {ticket_prefix}-
361
402
  Dict : {loaded — N canonical terms, M banned terms | missing}
362
403
  Entities : {loaded — EntityA, EntityB, EntityC | missing}
363
- Lessons : {loaded — N guardrails | chưa có}
404
+ Lessons : {loaded — {n} active cho lệnh này ({tổng} tổng) | chưa có}
405
+ {⚠️ CHỈ IN khi tổng ≥ 40: "{tổng} guardrail đang hoạt động — /learn --review để rà"}
364
406
  Platform : {active_platform: system | web | app | — nếu chưa xác định}
365
407
  Service : {active_service} ({active_service_module}) [← domain{/platform}{/prd_slug} nếu route qua by_prd_slug] | multi (map-theo-platform hoặc map-theo-prd_slug, chốt khi target đủ platform/prd_slug) | single-service
366
408
  Svc Root : {service_root} — đã nạp conventions + trace_dir từ config service | —