@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
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gate-trace — CỔNG CHẶN PR. Exit 1 khi trace có cờ 🔴.
4
+ *
5
+ * Vì sao tồn tại (GAPS-v3 G39): README khai bằng chữ in đậm rằng 4 cờ (ORPHANED ·
6
+ * TRACE_ORPHAN · SEAM_UNWIRED · STUB_UNRESOLVED) "chặn PR", kèm câu "build xanh + test
7
+ * từng-UC xanh KHÔNG phát hiện được" — đó là điểm bán hàng cốt lõi của framework.
8
+ * Nhưng trước file này KHÔNG CÓ GÌ CHẶN: 0 CI, 0 git hook, 0 exit code. /validate-traces
9
+ * in report vào terminal Claude Code, nơi không CI nào đọc được. Kỷ luật là honor-system,
10
+ * và cái gì không chặn thì sau sprint thứ ba không ai làm.
11
+ *
12
+ * Bốn tầng, theo thứ tự — mỗi tầng vô nghĩa nếu tầng trước chưa xanh:
13
+ *
14
+ * G0 Sổ có ĐƯỢC BẢO VỆ không? → nằm trong git? git biết cách gộp?
15
+ * Kiểm cấu trúc của một quyển sổ không nằm trong git thì vô nghĩa. Hai điều kiện
16
+ * này lint đã phát hiện dưới dạng WARN; ở đây chúng được NÂNG thành lỗi chặn —
17
+ * cố ý: pre-push nhắc, CI chặn (chặn push vì một dòng cấu hình sẽ dạy dev gõ
18
+ * --no-verify, và mất luôn phần lint thật).
19
+ * G1 Sổ có ĐÚNG HÌNH DẠNG không? → gọi bin/lint-trace.js (G38)
20
+ * Phán trạng thái trên sổ lệch cột là phán trên dữ liệu rác.
21
+ * G2 Report có TƯƠI không? → đối chiếu report ↔ sổ TSV
22
+ * Không có tầng này thì gate là sân khấu: chạy /validate-traces một lần, commit
23
+ * report, rồi sửa gì cũng được — CI đọc report cũ và cho qua mãi mãi.
24
+ * G3 Có cờ 🔴 nào không? → đếm counter khai trong schema `gate.blocking`
25
+ *
26
+ * PHẠM VI — nói thẳng để không ai tin quá:
27
+ * Gate chứng minh "report khớp SỔ". Chỉ /validate-traces chứng minh được "sổ khớp CODE"
28
+ * (nó cần quét tag trong source, đọc .feature, so version — việc của LLM, không phải
29
+ * của file này). Nên gate bắt được ca phổ biến "quên chạy lại", nhưng KHÔNG bắt được
30
+ * ca "sửa code mà không đụng sổ". Đường bịt: CI chạy /validate-traces, hoặc yêu cầu
31
+ * report có trong diff khi src đổi. §Output in ra đúng giới hạn này mỗi lần chạy.
32
+ *
33
+ * Cờ 🟠 (PRD_DRIFT, BDD_DRIFT, TECHDOC_DRIFT…) KHÔNG chặn — cố ý. Chúng là "code chưa
34
+ * theo kịp spec", một loại nợ khác. Chặn PR vì nợ là làm cổng bị tắt.
35
+ *
36
+ * Chạy: node bin/gate-trace.js [--trace DIR[,DIR]] [--report PATH]
37
+ * [--skip-lint] [--no-reconcile] [--warn-only] [--json]
38
+ * exit 1 nếu FAIL (0 với --warn-only)
39
+ */
40
+
41
+ const fs = require('fs');
42
+ const path = require('path');
43
+
44
+ const schema = JSON.parse(fs.readFileSync(path.join(__dirname, 'trace-schema.json'), 'utf8'));
45
+ const GATE = schema.gate;
46
+
47
+ if (!GATE || !Array.isArray(GATE.blocking) || !GATE.blocking.length) {
48
+ console.error('❌ bin/trace-schema.json thiếu khối `gate.blocking` — không biết chặn cờ nào.');
49
+ process.exit(1);
50
+ }
51
+
52
+ // ── CLI ───────────────────────────────────────────────────────────────────────
53
+
54
+ const argv = process.argv.slice(2);
55
+ const flag = (name, def) => {
56
+ const i = argv.indexOf(name);
57
+ return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : def;
58
+ };
59
+ const WARN_ONLY = argv.includes('--warn-only');
60
+ const AS_JSON = argv.includes('--json');
61
+ const SKIP_LINT = argv.includes('--skip-lint');
62
+ const NO_RECONCILE = argv.includes('--no-reconcile');
63
+ const TRACE_DIRS = flag('--trace', '.trace').split(',').map(s => s.trim()).filter(Boolean);
64
+ const REPORT_ARG = flag('--report', null);
65
+
66
+ const fails = [];
67
+ const warns = [];
68
+ const oks = [];
69
+ const fail = (stage, msg, hint) => fails.push({ stage, msg, hint });
70
+ const warn = (stage, msg, hint) => warns.push({ stage, msg, hint });
71
+
72
+ const rel = p => path.relative(process.cwd(), p).replace(/\\/g, '/');
73
+
74
+ // ── Chạy lint MỘT LẦN — G0 và G1 cùng đọc kết quả này ─────────────────────────
75
+
76
+ let lintSummary = 'bỏ qua (--skip-lint)';
77
+ let lintParsed = null;
78
+ let lintStderr = '';
79
+
80
+ if (!SKIP_LINT) {
81
+ const lintPath = path.join(__dirname, 'lint-trace.js');
82
+ const args = ['--trace', TRACE_DIRS.join(','), '--json'];
83
+ const r = require('child_process').spawnSync(process.execPath, [lintPath, ...args],
84
+ { encoding: 'utf8' });
85
+ lintStderr = r.stderr || '';
86
+ try { lintParsed = JSON.parse(r.stdout); } catch {}
87
+ }
88
+
89
+ // ── Tầng G0 — sổ có ĐƯỢC BẢO VỆ không (GAPS-v3 T10-debt) ──────────────────────
90
+ //
91
+ // Hỏi TRƯỚC G1 có lý do: G1 kiểm rất kỹ cấu trúc của một quyển sổ mà có thể KHÔNG NẰM
92
+ // TRONG GIT. Kiểm cấu trúc của thứ sắp mất thì vô nghĩa.
93
+ //
94
+ // Hai điều kiện đều đã được lint phát hiện dưới dạng WARN; ở đây chúng được NÂNG thành
95
+ // lỗi chặn. Một implementation, hai mức nghiêm trọng — cố ý: pre-push nhắc, CI chặn.
96
+ // Danh sách rule đọc từ schema (`gate.config_preconditions`), không hard-code — R9 canh.
97
+ const PRECOND = (GATE.config_preconditions || {}).promote_lint_rules || [];
98
+
99
+ if (SKIP_LINT) {
100
+ warn('G0', 'bỏ qua kiểm cấu hình (--skip-lint) — không biết sổ có được bảo vệ hay không');
101
+ } else if (!lintParsed) {
102
+ fail('G0', 'không chạy được bin/lint-trace.js — không xác nhận được sổ có được bảo vệ',
103
+ lintStderr.trim().split('\n').slice(0, 3).join('\n ')
104
+ || `Chạy tay để xem lỗi: node bin/lint-trace.js --trace ${TRACE_DIRS.join(',')}`);
105
+ } else {
106
+ const byRule = new Map(PRECOND.map(p => [p.rule, p.why]));
107
+ const hits = (lintParsed.warns || []).filter(w => byRule.has(w.rule));
108
+
109
+ for (const h of hits) {
110
+ fail('G0', `${h.where} — ${h.msg}`,
111
+ [`Vì sao chặn: ${byRule.get(h.rule)}`,
112
+ h.hint || ''].filter(Boolean).join('\n '));
113
+ }
114
+ if (!hits.length) {
115
+ oks.push(`G0 sổ được bảo vệ — nằm trong git · git biết cách gộp (${PRECOND.length}/${PRECOND.length} điều kiện)`);
116
+ }
117
+ }
118
+
119
+ // ── Tầng G1 — sổ có đúng hình dạng không (G38) ────────────────────────────────
120
+
121
+ if (!SKIP_LINT) {
122
+ const parsed = lintParsed;
123
+ if (!parsed) {
124
+ // G0 đã báo lý do — đừng nhân đôi thông điệp, chỉ ghi là không kiểm được.
125
+ fail('G1', 'không xác nhận được sổ có đúng hình dạng (lint không chạy được — xem G0)');
126
+ } else if (!parsed.ok) {
127
+ fail('G1', `sổ trace HỎNG CẤU TRÚC — ${parsed.errors.length} lỗi, gate không phán được`,
128
+ parsed.errors.slice(0, 6).map(e => `${e.where} — ${e.rule}: ${e.msg}`).join('\n ')
129
+ + (parsed.errors.length > 6 ? `\n … và ${parsed.errors.length - 6} lỗi nữa` : '')
130
+ + `\n Chi tiết: node bin/lint-trace.js --trace ${TRACE_DIRS.join(',')}`);
131
+ } else {
132
+ const rest = (parsed.warns || []).filter(w => !PRECOND.some(p => p.rule === w.rule));
133
+ lintSummary = `${parsed.scanned.books} sổ + ${parsed.scanned.seams} bảng phụ`
134
+ + (rest.length ? ` (${rest.length} warning)` : '');
135
+ oks.push(`G1 sổ đúng hình dạng — ${lintSummary}`);
136
+ // Warn đã được G0 nâng thành fail thì KHÔNG in lại ở đây.
137
+ for (const w of rest) warn('G1', `${w.where} — ${w.rule}: ${w.msg}`);
138
+ }
139
+ }
140
+
141
+ // ── Định vị report ────────────────────────────────────────────────────────────
142
+
143
+ const REPORT_BASENAME = path.basename(GATE.report_path.replace(/^.*[/\\]/, ''));
144
+
145
+ function findReport() {
146
+ if (REPORT_ARG) return fs.existsSync(REPORT_ARG) ? path.resolve(REPORT_ARG) : null;
147
+ for (const d of TRACE_DIRS) {
148
+ const p = path.resolve(d, REPORT_BASENAME);
149
+ if (fs.existsSync(p)) return p;
150
+ }
151
+ return null;
152
+ }
153
+
154
+ const reportPath = findReport();
155
+ let report = null;
156
+
157
+ if (!reportPath) {
158
+ fail('G2', `không tìm thấy ${REPORT_BASENAME} — trace CHƯA TỪNG được audit`,
159
+ [`Đã tìm ở: ${TRACE_DIRS.map(d => rel(path.resolve(d, REPORT_BASENAME))).join(' · ')}`,
160
+ `Gate không thể cho qua khi không có gì để đọc — "không có report" ≠ "không có lỗi".`,
161
+ `Chạy /validate-traces trong Claude Code, rồi commit ${REPORT_BASENAME}.`].join('\n '));
162
+ } else {
163
+ try {
164
+ report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
165
+ } catch (e) {
166
+ fail('G2', `${rel(reportPath)} không phải JSON hợp lệ: ${e.message}`,
167
+ `Nghi conflict merge chưa giải. trace-report.json là file SINH LẠI ĐƯỢC —\n`
168
+ + ` đừng merge tay, chạy lại /validate-traces.`);
169
+ }
170
+ }
171
+
172
+ // ── Tầng G2 — report có tươi không ────────────────────────────────────────────
173
+
174
+ /**
175
+ * Đọc mọi row dữ liệu của các sổ TSV (bỏ _seams.tsv — nó không mang scenario).
176
+ * Mỗi row kèm `platform` lấy từ TÊN FILE (`{UC-ID}-{platform}.tsv`) — đó là nơi duy
177
+ * nhất sổ mang platform, và là thứ làm khoá {sc_id, platform} của G48 dựng được.
178
+ */
179
+ function readBooks(dirs, domainFilter) {
180
+ const rows = [];
181
+ const pf = (schema.vocabularies.platform || {}).values || [];
182
+ const nameRe = pf.length ? new RegExp(`-(${pf.join('|')})\\.tsv$`) : null;
183
+
184
+ const walk = (d) => {
185
+ if (!fs.existsSync(d)) return;
186
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
187
+ const p = path.join(d, e.name);
188
+ if (e.isDirectory()) { walk(p); continue; }
189
+ if (!e.name.endsWith('.tsv') || e.name === '_seams.tsv') continue;
190
+ const m = nameRe && nameRe.exec(e.name);
191
+ const platform = m ? m[1] : '';
192
+ const lines = fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n').split('\n')
193
+ .filter(l => l.trim() !== '');
194
+ if (lines.length < 2) continue;
195
+ const header = lines[0].split('\t').map(s => s.trim());
196
+ const si = header.indexOf('status');
197
+ const ii = header.indexOf('sc_id');
198
+ for (const l of lines.slice(1)) {
199
+ const f = l.split('\t');
200
+ rows.push({
201
+ file: p, platform,
202
+ sc_id: ii !== -1 ? (f[ii] || '').trim() : '',
203
+ status: si !== -1 ? (f[si] || '').trim() : '',
204
+ });
205
+ }
206
+ }
207
+ };
208
+ for (const d of dirs) {
209
+ const abs = path.resolve(d);
210
+ // Report chỉ phủ một domain → chỉ đọc sổ của domain đó, else so lệch là chắc chắn.
211
+ walk(domainFilter && domainFilter !== 'all' ? path.join(abs, domainFilter) : abs);
212
+ }
213
+ return rows;
214
+ }
215
+
216
+ /** Duỗi cây prds[].ucs[].scenarios[] của report thành danh sách phẳng. */
217
+ function flattenReportScenarios(report) {
218
+ const out = [];
219
+ for (const prd of report.prds || []) {
220
+ for (const uc of prd.ucs || []) {
221
+ for (const sc of uc.scenarios || []) out.push(sc);
222
+ }
223
+ }
224
+ return out;
225
+ }
226
+
227
+ /** Domain nào có sổ trên đĩa — để bắt ca report chỉ phủ một phần repo. */
228
+ function domainsOnDisk(dirs) {
229
+ const out = new Set();
230
+ for (const d of dirs) {
231
+ const abs = path.resolve(d);
232
+ if (!fs.existsSync(abs)) continue;
233
+ for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
234
+ if (e.isDirectory() && !e.name.startsWith('.')) out.add(e.name);
235
+ }
236
+ }
237
+ return [...out];
238
+ }
239
+
240
+ if (report && !NO_RECONCILE) {
241
+ const domain = report.domain || 'all';
242
+ const summary = report.summary || {};
243
+
244
+ // (a) Report phủ một phần repo → cờ 🔴 ở phần chưa audit hoàn toàn vô hình với gate.
245
+ // Cho qua ở đây là cấp một tờ giấy xanh cho thứ chưa ai xem.
246
+ //
247
+ // G57 — LUẬT TUYỆT ĐỐI, không phụ thuộc trên đĩa có bao nhiêu domain.
248
+ // Bản cũ hỏi "còn domain NÀO KHÁC không": `if (others.length) fail`. Trong repo MỘT domain,
249
+ // `others` là RỖNG ⇒ KHÔNG fail ⇒ một report hẹp-theo-PRD (hoặc theo-UC) được nhận là "toàn
250
+ // bộ". Lúc chưa có cờ scope thì vô hại; nó BẬT NGAY khi scoping thành khả dụng — tức bản fix
251
+ // G57 sẽ tự tay mở đúng cái false-green mà chú thích ngay trên cảnh báo.
252
+ // Nên: scope.kind !== 'all' ⇒ FAIL. Hết.
253
+ const scope = report.scope;
254
+ if (scope && typeof scope === 'object') {
255
+ if (scope.kind !== 'all') {
256
+ const what = scope.value && scope.value !== 'all'
257
+ ? `${scope.kind}="${scope.value}"` : String(scope.kind);
258
+ fail('G2', `report chỉ phủ ${what} — đây KHÔNG phải một audit đầy đủ`,
259
+ ['Cờ 🔴 ngoài phạm vi đó hoàn toàn vô hình với gate.',
260
+ 'Gate cho qua lúc này là khẳng định sạch cho thứ chưa ai xem.',
261
+ 'Chạy /validate-traces KHÔNG kèm --domain/--prd/--uc, rồi commit report.'].join('\n '));
262
+ }
263
+ } else if (domain !== 'all') {
264
+ // TƯƠNG THÍCH NGƯỢC: report sinh trước G57 không có `scope`. Rơi về logic cũ theo `domain`
265
+ // và nói rõ kiểm đang ở mức YẾU — KHÔNG fail vì thiếu field. Người dùng chưa chạy lại
266
+ // /validate-traces không phải là lỗi của họ. Cùng khuôn G48 đã dùng cho `platform`.
267
+ warn('G2', 'report không có field `scope` (sinh trước G57) — kiểm độ phủ đang ở mức YẾU',
268
+ 'Chạy lại /validate-traces để report mang `scope`; chỉ khi đó gate mới chặn được mọi\n'
269
+ + ' loại audit phiến diện, kể cả hẹp-theo-PRD trong repo một domain.');
270
+ const others = domainsOnDisk(TRACE_DIRS).filter(d => d !== domain);
271
+ if (others.length) {
272
+ fail('G2', `report chỉ phủ domain "${domain}" — còn ${others.length} domain có sổ mà chưa được audit`,
273
+ [`Chưa phủ: ${others.join(' · ')}`,
274
+ `Gate cho qua lúc này là khẳng định sạch cho thứ chưa ai xem.`,
275
+ `Chạy /validate-traces KHÔNG kèm domain để audit toàn bộ, rồi commit report.`].join('\n '));
276
+ }
277
+ }
278
+
279
+ const rows = readBooks(TRACE_DIRS, domain);
280
+
281
+ // (b) hard — số row là tuyệt đối, không nhập nhằng quy ước.
282
+ const hardKeys = Object.keys(GATE.reconcile.hard || {});
283
+ for (const key of hardKeys) {
284
+ if (!(key in summary)) {
285
+ fail('G2', `report thiếu \`summary.${key}\` — không đối chiếu được độ tươi`,
286
+ `Counter bị đổi tên? Sửa bin/trace-schema.json \`gate.reconcile\` cho khớp\n`
287
+ + ` (và commands/validate-traces.tmpl khối \`summary\`).`);
288
+ continue;
289
+ }
290
+ if (summary[key] !== rows.length) {
291
+ fail('G2', `report CŨ — \`summary.${key}\` = ${summary[key]} nhưng sổ TSV đang có ${rows.length} scenario`,
292
+ [`Sổ đã thay đổi sau lần audit cuối (${report.generated_at || 'không rõ thời điểm'}).`,
293
+ `Mọi con số dưới đây — gồm cả các cờ 🔴 — nói về một trạng thái đã qua.`,
294
+ `Chạy /validate-traces rồi commit lại ${REPORT_BASENAME}.`].join('\n '));
295
+ }
296
+ }
297
+
298
+ // (c) soft — phụ thuộc quy ước đếm, nên WARN chứ không FAIL. Sai ở đây thì tệ nhất là
299
+ // ồn; ép cứng mà lệch quy ước thì cổng bị tắt — hỏng nặng hơn nhiều.
300
+ const byStatus = {};
301
+ for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
302
+
303
+ for (const [key, statusVal] of Object.entries(GATE.reconcile.soft || {})) {
304
+ if (!(key in summary)) continue;
305
+ const onDisk = byStatus[statusVal] || 0;
306
+ if (summary[key] !== onDisk) {
307
+ warn('G2', `\`summary.${key}\` = ${summary[key]} nhưng sổ có ${onDisk} row status=${statusVal}`,
308
+ `Report cũ, hoặc quy ước đếm khác. Không chặn — chạy /validate-traces để chắc.`);
309
+ }
310
+ }
311
+
312
+ // (d) per-SC (G48) — đối chiếu TỪNG scenario, không chỉ tổng số.
313
+ //
314
+ // So tổng bỏ lọt ca thường gặp nhất khi spec tiến hoá: một SC bị xoá + một SC được
315
+ // thêm trong cùng một lần ⇒ total_scs KHÔNG đổi ⇒ report cũ vẫn được coi là tươi.
316
+ // Khoá phải gồm platform: sc_id một mình không định danh được scenario.
317
+ const PS = GATE.reconcile.per_sc;
318
+ if (PS) {
319
+ const scs = flattenReportScenarios(report);
320
+ const hasPlatform = scs.length > 0 && scs.every(s => s[PS.key_fields[1]]);
321
+
322
+ if (!scs.length) {
323
+ warn('G2', 'report không kèm `prds[].ucs[].scenarios[]` — chỉ đối chiếu được tổng số',
324
+ 'Kiểm độ tươi đang ở mức yếu: một SC bị xoá + một SC được thêm sẽ không bị phát hiện.');
325
+ } else if (!hasPlatform) {
326
+ // Backward-compat: report sinh bởi version trước G48. Không fail — người dùng chưa
327
+ // chạy lại /validate-traces không phải lỗi của họ.
328
+ warn('G2', 'report thiếu `platform` trong `scenarios[]` — đối chiếu chỉ ở mức tổng số',
329
+ [`Report sinh bởi version trước G48. sc_id một mình không phân biệt được web/app,`,
330
+ `nên không khoá được từng scenario.`,
331
+ `Chạy lại /validate-traces bằng version hiện tại để bật đối chiếu đầy đủ.`].join('\n '));
332
+ } else {
333
+ const key = (o) => PS.key_fields.map(f => o[f]).join('@');
334
+ // Quy đổi ORPHANED: JSON cố ý chỉ có 4 giá trị status; row ORPHANED xuất ra là
335
+ // `status: DRIFT` + `orphaned: true` (validate-traces §Rules). Phải quy đổi TRƯỚC
336
+ // khi so, không thì mọi row ORPHANED đều báo lệch giả.
337
+ const norm = (s) => (s[PS.orphaned_flag] === true ? PS.orphaned_status : s[PS.compare]);
338
+
339
+ const inReport = new Map(scs.map(s => [key(s), norm(s)]));
340
+ const inBook = new Map(rows.map(r => [key(r), r[PS.compare]]));
341
+
342
+ const missing = [...inBook.keys()].filter(k => !inReport.has(k));
343
+ const extra = [...inReport.keys()].filter(k => !inBook.has(k));
344
+ const differ = [...inBook.entries()]
345
+ .filter(([k, v]) => inReport.has(k) && inReport.get(k) !== v)
346
+ .map(([k, v]) => `${k}: sổ=${v} report=${inReport.get(k)}`);
347
+
348
+ const show = (a) => a.slice(0, 6).join(' · ') + (a.length > 6 ? ` … +${a.length - 6}` : '');
349
+
350
+ if (missing.length || extra.length) {
351
+ fail('G2', `report CŨ — lệch ${missing.length + extra.length} scenario so với sổ`,
352
+ [missing.length ? `Có trong sổ, thiếu ở report: ${show(missing)}` : null,
353
+ extra.length ? `Có ở report, không còn trong sổ: ${show(extra)}` : null,
354
+ `Sổ đã thay đổi sau lần audit cuối (${report.generated_at || 'không rõ'}).`,
355
+ `Chạy /validate-traces rồi commit lại ${REPORT_BASENAME}.`].filter(Boolean).join('\n '));
356
+ } else if (differ.length) {
357
+ fail('G2', `report CŨ — ${differ.length} scenario khác trạng thái so với sổ`,
358
+ [show(differ),
359
+ `Cùng tập scenario nhưng status đã đổi ⇒ audit cuối nói về một trạng thái đã qua.`,
360
+ `Chạy /validate-traces rồi commit lại ${REPORT_BASENAME}.`].join('\n '));
361
+ }
362
+ }
363
+ }
364
+
365
+ if (!fails.some(f => f.stage === 'G2')) {
366
+ // `[].every()` là true — nên phải đòi CÓ scenario, không thì report rỗng sẽ khoe
367
+ // "đối chiếu từng SC" trong khi nó chẳng đối chiếu gì.
368
+ const scsNow = PS ? flattenReportScenarios(report) : [];
369
+ const PSok = scsNow.length > 0 && scsNow.every(s => s[PS.key_fields[1]]);
370
+ oks.push(`G2 report tươi — ${rows.length} scenario khớp sổ`
371
+ + (PSok ? ' (đối chiếu từng SC)' : ' (chỉ tổng số)')
372
+ + ` · audit lúc ${report.generated_at || '?'}`
373
+ + (domain !== 'all' ? ` · domain "${domain}"` : ''));
374
+ }
375
+ } else if (report && NO_RECONCILE) {
376
+ warn('G2', 'bỏ qua kiểm độ tươi (--no-reconcile) — report có thể đã cũ',
377
+ 'Cờ 🔴 dưới đây có thể nói về một trạng thái đã qua.');
378
+ }
379
+
380
+ // ── Tầng G3 — cờ 🔴 ───────────────────────────────────────────────────────────
381
+
382
+ const blocking = [];
383
+
384
+ if (report) {
385
+ const summary = report.summary || {};
386
+ const issues = report.issues || {};
387
+
388
+ for (const b of GATE.blocking) {
389
+ // Counter VẮNG MẶT không được coi là 0. `undefined > 0` là false ⇒ gate sẽ PASS trên
390
+ // code hỏng, im lặng. Đúng hình dạng G1 (consumer trỏ vào chỗ trống), và ở đây hậu quả
391
+ // là một PR hỏng được merge kèm dấu ✅.
392
+ if (!(b.counter in summary)) {
393
+ fail('G3', `report thiếu \`summary.${b.counter}\` — cờ này KHÔNG được kiểm`,
394
+ [`Không suy ra là 0: counter vắng mặt nghĩa là gate MÙ với cờ này, không phải sạch.`,
395
+ `Nguyên nhân: report sinh bởi version cũ, hoặc counter đã đổi tên.`,
396
+ `Sửa: chạy lại /validate-traces bằng version framework hiện tại. Nếu counter đổi tên`,
397
+ `thật thì cập nhật bin/trace-schema.json \`gate.blocking\` — self-check R9 canh chỗ đó.`].join('\n '));
398
+ continue;
399
+ }
400
+
401
+ const n = Number(summary[b.counter]) || 0;
402
+ if (n === 0) continue;
403
+
404
+ const rows = Array.isArray(issues[b.issues_key]) ? issues[b.issues_key] : [];
405
+ blocking.push({
406
+ counter: b.counter, count: n, why: b.why, issues_key: b.issues_key,
407
+ items: rows.map(r => ({
408
+ label: (b.label_fields || []).map(f => r[f]).filter(Boolean).join(' · ') || '(không rõ)',
409
+ fix: r.fix || null,
410
+ })),
411
+ });
412
+ }
413
+
414
+ if (blocking.length) {
415
+ const total = blocking.reduce((s, b) => s + b.count, 0);
416
+ fail('G3', `${total} vấn đề 🔴 — code đang hỏng, KHÔNG merge`, null);
417
+ } else if (!fails.length) {
418
+ oks.push(`G3 không có cờ 🔴 — ${GATE.blocking.map(b => b.counter).length}/${GATE.blocking.length} cờ chặn đều bằng 0`);
419
+ }
420
+ }
421
+
422
+ // ── Output ────────────────────────────────────────────────────────────────────
423
+
424
+ const passed = fails.length === 0;
425
+
426
+ if (AS_JSON) {
427
+ console.log(JSON.stringify({
428
+ ok: passed,
429
+ report: reportPath ? rel(reportPath) : null,
430
+ generated_at: report && report.generated_at || null,
431
+ blocking, fails, warns,
432
+ }, null, 2));
433
+ process.exit(passed || WARN_ONLY ? 0 : 1);
434
+ }
435
+
436
+ console.log('');
437
+ console.log('Trace gate (bin/trace-schema.json → gate) ...');
438
+ console.log('');
439
+
440
+ for (const o of oks) console.log(` ✅ ${o}`);
441
+ if (oks.length) console.log('');
442
+
443
+ for (const w of warns) {
444
+ console.log(` ⚠️ [${w.stage}] ${w.msg}`);
445
+ if (w.hint) console.log(` ${w.hint}`);
446
+ }
447
+ if (warns.length) console.log('');
448
+
449
+ for (const f of fails) {
450
+ console.log(` 🔴 [${f.stage}] ${f.msg}`);
451
+ if (f.hint) console.log(` ${f.hint}`);
452
+ console.log('');
453
+ }
454
+
455
+ for (const b of blocking) {
456
+ console.log(` 🔴 ${b.counter} = ${b.count} — ${b.why}`);
457
+ for (const it of b.items.slice(0, 8)) {
458
+ console.log(` • ${it.label}`);
459
+ if (it.fix) console.log(` → ${it.fix}`);
460
+ }
461
+ if (b.items.length > 8) console.log(` … và ${b.items.length - 8} nữa`);
462
+ if (!b.items.length) {
463
+ console.log(` (report không kèm chi tiết trong \`issues.${b.issues_key}\` — chạy /validate-traces để xem)`);
464
+ }
465
+ console.log('');
466
+ }
467
+
468
+ if (passed) {
469
+ console.log(' ✅ TRACE GATE PASSED');
470
+ console.log('');
471
+ // In giới hạn MỖI LẦN, kể cả khi xanh. Một cổng không nói rõ nó chưa chứng minh gì
472
+ // sẽ được người đọc hiểu là đã chứng minh mọi thứ — và đó là cách một cổng thật biến
473
+ // thành một cổng giả mà không ai sửa dòng code nào.
474
+ console.log(' Cổng này chứng minh: report khớp SỔ, và sổ không có cờ 🔴.');
475
+ console.log(' Cổng này KHÔNG chứng minh: sổ khớp CODE — việc đó cần /validate-traces');
476
+ console.log(' (quét tag trong source, đọc .feature, so version). Sửa code mà không đụng');
477
+ console.log(' sổ thì cổng vẫn xanh. Cho CI đầy đủ: chạy /validate-traces trước bước này.');
478
+ } else {
479
+ console.log(' 🔴 TRACE GATE FAILED');
480
+ console.log('');
481
+ console.log(' Đây là lớp lỗi mà build xanh + test từng-UC xanh KHÔNG phát hiện được:');
482
+ console.log(' luồng ghép chạy vào hàm rỗng, hoặc code trỏ vào scenario đã bị xoá.');
483
+ if (WARN_ONLY) console.log(' (--warn-only: exit 0 dù có lỗi)');
484
+ }
485
+ console.log('');
486
+
487
+ process.exit(passed || WARN_ONLY ? 0 : 1);