@longph2102/v-flow 1.5.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 (247) hide show
  1. package/AGENTS.md +265 -0
  2. package/CHANGELOG.md +318 -0
  3. package/LICENSE +21 -0
  4. package/README.md +326 -0
  5. package/agents/ba-agent.md +437 -0
  6. package/agents/ba-critic-agent.md +156 -0
  7. package/agents/ba-to-ptyc-agent.md +112 -0
  8. package/agents/bugfix-analyst-agent.md +221 -0
  9. package/agents/constitute-agent.md +155 -0
  10. package/agents/help-agent.md +168 -0
  11. package/agents/implement-agent.md +220 -0
  12. package/agents/import-ba-docs-agent.md +164 -0
  13. package/agents/master-check-agent.md +228 -0
  14. package/agents/metrics-agent.md +180 -0
  15. package/agents/operations-agent.md +123 -0
  16. package/agents/plan-agent.md +218 -0
  17. package/agents/prototype-agent.md +191 -0
  18. package/agents/retrospective-agent.md +196 -0
  19. package/agents/review-agent.md +210 -0
  20. package/agents/sprint-agent.md +191 -0
  21. package/agents/status-agent.md +186 -0
  22. package/agents/sync-agent.md +201 -0
  23. package/agents/test-agent.md +166 -0
  24. package/agents/understand-agent.md +339 -0
  25. package/cli/commands/check.js +96 -0
  26. package/cli/commands/dev-quiz.js +107 -0
  27. package/cli/commands/doctor.js +348 -0
  28. package/cli/commands/feature.js +259 -0
  29. package/cli/commands/hooks.js +163 -0
  30. package/cli/commands/init.js +189 -0
  31. package/cli/commands/log.js +199 -0
  32. package/cli/commands/plugin.js +230 -0
  33. package/cli/commands/score-card.js +203 -0
  34. package/cli/commands/status.js +269 -0
  35. package/cli/commands/sync.js +59 -0
  36. package/cli/commands/upgrade.js +150 -0
  37. package/cli/commands/validate.js +1259 -0
  38. package/cli/commands/watch.js +151 -0
  39. package/cli/index.js +46 -0
  40. package/cli/lib/ac-test-gate.js +89 -0
  41. package/cli/lib/activity-log.js +209 -0
  42. package/cli/lib/cli-error.js +183 -0
  43. package/cli/lib/constitution-lint.js +561 -0
  44. package/cli/lib/dev-quiz-grade.js +127 -0
  45. package/cli/lib/governance.js +78 -0
  46. package/cli/lib/hook-targets.js +167 -0
  47. package/cli/lib/i18n.js +375 -0
  48. package/cli/lib/knowledge-oracle.js +379 -0
  49. package/cli/lib/logger.js +203 -0
  50. package/cli/lib/module-card-lint.js +304 -0
  51. package/cli/lib/module-card-score.js +223 -0
  52. package/cli/lib/plugins.js +481 -0
  53. package/cli/lib/scanner.js +692 -0
  54. package/cli/lib/sync-core.js +232 -0
  55. package/cli/lib/sync-targets.js +84 -0
  56. package/cli/lib/templates.js +268 -0
  57. package/cli/lib/yaml-parser.js +203 -0
  58. package/commands/v.ba-critic.md +101 -0
  59. package/commands/v.ba-to-ptyc.md +71 -0
  60. package/commands/v.bugfix.md +86 -0
  61. package/commands/v.check.md +131 -0
  62. package/commands/v.constitute.md +87 -0
  63. package/commands/v.constitution.md +84 -0
  64. package/commands/v.fork.md +127 -0
  65. package/commands/v.help.md +73 -0
  66. package/commands/v.hotfix.md +200 -0
  67. package/commands/v.implement.md +92 -0
  68. package/commands/v.import-ba-docs.md +222 -0
  69. package/commands/v.metrics.md +74 -0
  70. package/commands/v.operations.md +70 -0
  71. package/commands/v.plan.md +78 -0
  72. package/commands/v.prototype.md +121 -0
  73. package/commands/v.quickfix.md +169 -0
  74. package/commands/v.retrospective.md +80 -0
  75. package/commands/v.review.md +78 -0
  76. package/commands/v.rewind.md +127 -0
  77. package/commands/v.specify.md +118 -0
  78. package/commands/v.sprint.md +75 -0
  79. package/commands/v.status.md +62 -0
  80. package/commands/v.sync.md +81 -0
  81. package/commands/v.test.md +67 -0
  82. package/commands/v.understand.md +112 -0
  83. package/package.json +65 -0
  84. package/skills/_shared/constitution-reader/SKILL.md +109 -0
  85. package/skills/_shared/constitution-reader/config.json +52 -0
  86. package/skills/_shared/constitution-reader/examples/good/b1-phase-output.md +48 -0
  87. package/skills/_shared/constitution-reader/gotchas.md +46 -0
  88. package/skills/_shared/context-reader/SKILL.md +111 -0
  89. package/skills/_shared/context-reader/config.json +54 -0
  90. package/skills/_shared/context-reader/examples/good/legacy-nodejs-output.md +35 -0
  91. package/skills/_shared/context-reader/gotchas.md +49 -0
  92. package/skills/_shared/ears-notation/SKILL.md +63 -0
  93. package/skills/_shared/ears-notation/config.json +55 -0
  94. package/skills/_shared/ears-notation/examples/good/plan-test-interpretation.md +29 -0
  95. package/skills/_shared/ears-notation/gotchas.md +43 -0
  96. package/skills/check/cross-validator/SKILL.md +206 -0
  97. package/skills/check/cross-validator/config.json +33 -0
  98. package/skills/check/cross-validator/examples/good/validation-report-pass-with-concerns.md +105 -0
  99. package/skills/check/cross-validator/gotchas.md +43 -0
  100. package/skills/implement/constitution-enforcer/SKILL.md +134 -0
  101. package/skills/implement/constitution-enforcer/config.json +16 -0
  102. package/skills/implement/constitution-enforcer/examples/bad/vague-report.md +42 -0
  103. package/skills/implement/constitution-enforcer/examples/good/compliance-report.md +57 -0
  104. package/skills/implement/constitution-enforcer/gotchas.md +26 -0
  105. package/skills/implement/constitution-enforcer/scripts/check-constitution.sh +88 -0
  106. package/skills/implement/no-go-zone-guard/SKILL.md +173 -0
  107. package/skills/implement/no-go-zone-guard/config.json +28 -0
  108. package/skills/implement/no-go-zone-guard/examples/good/adapter-workaround.md +46 -0
  109. package/skills/implement/no-go-zone-guard/gotchas.md +27 -0
  110. package/skills/implement/no-go-zone-guard/scripts/check-nogo-zones.sh +148 -0
  111. package/skills/implement/no-go-zone-guard/scripts/nogo-precommit.sh +96 -0
  112. package/skills/implement/tdd-driver/SKILL.md +159 -0
  113. package/skills/implement/tdd-driver/config.json +33 -0
  114. package/skills/implement/tdd-driver/examples/good/tdd-cycle-product-repo.md +81 -0
  115. package/skills/implement/tdd-driver/gotchas.md +34 -0
  116. package/skills/metrics/metrics-collector/SKILL.md +133 -0
  117. package/skills/metrics/metrics-collector/config.json +16 -0
  118. package/skills/metrics/metrics-collector/examples/bad/incomplete-report.md +48 -0
  119. package/skills/metrics/metrics-collector/examples/good/full-metrics-report.md +101 -0
  120. package/skills/metrics/metrics-collector/gotchas.md +26 -0
  121. package/skills/operations/incident-runbook/SKILL.md +167 -0
  122. package/skills/operations/incident-runbook/config.json +21 -0
  123. package/skills/operations/incident-runbook/examples/bad/vague-incident-report.md +48 -0
  124. package/skills/operations/incident-runbook/examples/good/p1-hotfix-response.md +119 -0
  125. package/skills/operations/incident-runbook/gotchas.md +26 -0
  126. package/skills/plan/architecture-designer/SKILL.md +228 -0
  127. package/skills/plan/architecture-designer/config.json +32 -0
  128. package/skills/plan/architecture-designer/examples/bad/vague-plan.md +62 -0
  129. package/skills/plan/architecture-designer/examples/good/expand-contract-migration.md +56 -0
  130. package/skills/plan/architecture-designer/examples/good/plan-structure.md +58 -0
  131. package/skills/plan/architecture-designer/gotchas.md +45 -0
  132. package/skills/plan/task-breakdown/SKILL.md +208 -0
  133. package/skills/plan/task-breakdown/config.json +26 -0
  134. package/skills/plan/task-breakdown/examples/bad/vague-tasks.md +77 -0
  135. package/skills/plan/task-breakdown/examples/good/spike-clarify-tasks.md +66 -0
  136. package/skills/plan/task-breakdown/examples/good/tasks-login-feature.md +111 -0
  137. package/skills/plan/task-breakdown/gotchas.md +39 -0
  138. package/skills/prototype/LOGIC.md +240 -0
  139. package/skills/prototype/SKILL.md +185 -0
  140. package/skills/prototype/UI.md +407 -0
  141. package/skills/prototype/config.json +104 -0
  142. package/skills/prototype/examples/bad/prototype-notes.md +68 -0
  143. package/skills/prototype/examples/good/prototype-notes-ui.md +109 -0
  144. package/skills/prototype/examples/good/prototype-notes.md +67 -0
  145. package/skills/prototype/gotchas.md +128 -0
  146. package/skills/prototype/scripts/check-flow-state.ps1 +112 -0
  147. package/skills/prototype/scripts/check-flow-state.sh +104 -0
  148. package/skills/prototype/scripts/check-prototype-cleanup.ps1 +124 -0
  149. package/skills/prototype/scripts/check-prototype-cleanup.sh +109 -0
  150. package/skills/prototype/scripts/check-prototype-notes.ps1 +107 -0
  151. package/skills/prototype/scripts/check-prototype-notes.sh +102 -0
  152. package/skills/review/adversarial-reviewer/SKILL.md +137 -0
  153. package/skills/review/adversarial-reviewer/config.json +32 -0
  154. package/skills/review/adversarial-reviewer/examples/good/review-report-template.md +56 -0
  155. package/skills/review/adversarial-reviewer/gotchas.md +46 -0
  156. package/skills/review/adversarial-reviewer/scripts/quick-security-scan.sh +52 -0
  157. package/skills/specify/ba-bpmn-doc-gen/SKILL.md +108 -0
  158. package/skills/specify/ba-bpmn-doc-gen/reference/reference-bpmn-generation.md +528 -0
  159. package/skills/specify/ba-bpmn-doc-gen/reference/reference-drawio-flowchart.md +466 -0
  160. package/skills/specify/ba-critic/SKILL.md +172 -0
  161. package/skills/specify/ba-critic/config.json +32 -0
  162. package/skills/specify/ba-critic/examples/good/critic-report-round1.md +51 -0
  163. package/skills/specify/ba-critic/gotchas.md +40 -0
  164. package/skills/specify/ba-critic/scripts/check-spec-quality.sh +72 -0
  165. package/skills/specify/ba-doc-generator/SKILL.md +102 -0
  166. package/skills/specify/ba-doc-generator/references/template-clevel.md +84 -0
  167. package/skills/specify/ba-doc-generator/references/template-compliance.md +83 -0
  168. package/skills/specify/ba-doc-generator/references/template-dev.md +138 -0
  169. package/skills/specify/ba-doc-generator/references/template-partner.md +167 -0
  170. package/skills/specify/ba-doc-generator/references/template-pm.md +92 -0
  171. package/skills/specify/ba-doc-generator/references/template-review.md +114 -0
  172. package/skills/specify/ba-doc-generator/references/template-tester.md +108 -0
  173. package/skills/specify/ba-doc-generator/references/template-user.md +98 -0
  174. package/skills/specify/bugfix-analyst/SKILL.md +296 -0
  175. package/skills/specify/bugfix-analyst/config.json +41 -0
  176. package/skills/specify/bugfix-analyst/examples/bad/common-mistakes.md +71 -0
  177. package/skills/specify/bugfix-analyst/examples/good/email-validation-bugfix.md +53 -0
  178. package/skills/specify/bugfix-analyst/gotchas.md +51 -0
  179. package/skills/specify/ears-writer/SKILL.md +129 -0
  180. package/skills/specify/ears-writer/config.json +20 -0
  181. package/skills/specify/ears-writer/examples/bad/common-mistakes.md +17 -0
  182. package/skills/specify/ears-writer/examples/good/login-requirements.md +41 -0
  183. package/skills/specify/ears-writer/gotchas.md +43 -0
  184. package/skills/specify/ears-writer/scripts/check-ears-compliance.sh +51 -0
  185. package/skills/test/test-case-generator/SKILL.md +161 -0
  186. package/skills/test/test-case-generator/config.json +33 -0
  187. package/skills/test/test-case-generator/examples/good/test-cases-login.md +104 -0
  188. package/skills/test/test-case-generator/gotchas.md +43 -0
  189. package/skills/understand/ba-docs-scanner/SKILL.md +239 -0
  190. package/skills/understand/ba-docs-scanner/config.json +47 -0
  191. package/skills/understand/ba-docs-scanner/examples/good/work-order-br-extract.md +28 -0
  192. package/skills/understand/ba-docs-scanner/gotchas.md +44 -0
  193. package/skills/understand/ba-docs-scanner/merge-rules.md +47 -0
  194. package/skills/understand/codebase-scanner/SKILL.md +260 -0
  195. package/skills/understand/codebase-scanner/config.json +56 -0
  196. package/skills/understand/codebase-scanner/examples/good/menu-module-output.md +44 -0
  197. package/skills/understand/codebase-scanner/gotchas.md +42 -0
  198. package/skills/understand/codebase-scanner/scripts/scan-project-structure.sh +64 -0
  199. package/templates/DESIGN.md +456 -0
  200. package/templates/agent-command-template.yaml +240 -0
  201. package/templates/agent-config-template.md +170 -0
  202. package/templates/agent-definition-template.md +145 -0
  203. package/templates/agent-metrics-template.md +150 -0
  204. package/templates/api-contract-template.md +72 -0
  205. package/templates/bugfix-report-template.md +195 -0
  206. package/templates/bugfix-spec-template.md +134 -0
  207. package/templates/code-review-report-template.md +119 -0
  208. package/templates/constitution-template.md +234 -0
  209. package/templates/context-template.md +94 -0
  210. package/templates/data-model-template.md +95 -0
  211. package/templates/decision-log-template.md +92 -0
  212. package/templates/flow-state-template.yaml +208 -0
  213. package/templates/github/workflows/v-flow-validate.yml +30 -0
  214. package/templates/knowledge/adr-template.md +70 -0
  215. package/templates/knowledge/api-contract-template.md +140 -0
  216. package/templates/knowledge/domain-glossary.md +29 -0
  217. package/templates/knowledge/golden-tests-readme.md +115 -0
  218. package/templates/knowledge/lessons-learned.md +41 -0
  219. package/templates/knowledge/patterns.md +103 -0
  220. package/templates/module-card/SKILL.md +85 -0
  221. package/templates/module-card/api-specs.md +96 -0
  222. package/templates/module-card/business-quiz.md +119 -0
  223. package/templates/module-card/cross-service.md +125 -0
  224. package/templates/module-card/db.md +85 -0
  225. package/templates/module-card/dev-quiz.md +62 -0
  226. package/templates/module-card/permissions.md +83 -0
  227. package/templates/module-card/state-diagram.md +64 -0
  228. package/templates/module-card/tech-context.md +90 -0
  229. package/templates/module-card/ui-flows.md +91 -0
  230. package/templates/module-card/use-cases.md +142 -0
  231. package/templates/module-template.yaml +161 -0
  232. package/templates/operations-report-template.md +108 -0
  233. package/templates/plan-template.md +308 -0
  234. package/templates/prototype-notes-template.md +116 -0
  235. package/templates/ptyc/PTYC.template.docx +0 -0
  236. package/templates/ptyc/ptyc.meta.example.yaml +44 -0
  237. package/templates/retrospective-report-template.md +136 -0
  238. package/templates/security-review-template.md +84 -0
  239. package/templates/session-template.md +167 -0
  240. package/templates/spec-review-log-template.md +75 -0
  241. package/templates/spec-template.md +229 -0
  242. package/templates/sprint-status-template.md +101 -0
  243. package/templates/tasks-template.md +275 -0
  244. package/templates/test-cases-template.md +124 -0
  245. package/templates/ux-checklist-template.md +79 -0
  246. package/templates/validation-report-template.md +125 -0
  247. package/templates/vflow-config-template.yaml +22 -0
@@ -0,0 +1,1259 @@
1
+ /**
2
+ * v-flow validate — Validate V-Flow artifact structure and quality
3
+ *
4
+ * Checks spec/plan/tasks/test-cases for required sections, EARS compliance,
5
+ * REQ/AC traceability, dependency-graph integrity, and cross-file consistency
6
+ * (spec↔plan↔tasks).
7
+ *
8
+ * SCOPE — đọc trước khi mở rộng: đây là LINT CẤU TRÚC + CROSS-REF, KHÔNG phải
9
+ * kiểm tra ngữ nghĩa. Cố ý KHÔNG verify "Done khi có khớp nội dung AC không",
10
+ * "design có đúng không"… — đó là việc của Master Check Agent (R.0, agent-driven)
11
+ * + human review. Đừng đẩy validator sang semantic-checking: markdown-first + regex
12
+ * là chủ ý cho pipeline LLM. Phân vai: validator = structural lint · Master Check =
13
+ * semantic · human = judgment.
14
+ *
15
+ * Phạm vi lint: spec / plan / tasks / test-cases (4 artifact lõi). Các artifact phụ
16
+ * data-model / api-contract / ux-checklist / validation-report KHÔNG lint cấu trúc ở
17
+ * đây — coverage/đúng-sai của chúng do Master Check (R.0) + human chịu trách nhiệm.
18
+ */
19
+
20
+ const path = require('path');
21
+ const fs = require('fs-extra');
22
+ const chalk = require('chalk');
23
+ const { isInitialized, getVFlowDir, listFeatures } = require('../lib/templates');
24
+ const { withErrorHandler, requireInit, printOk, printWarn, printError, printDim } = require('../lib/cli-error');
25
+ const { createT } = require('../lib/i18n');
26
+ const { parseConstitution, runConstitutionLint, formatViolation, SEVERITY } = require('../lib/constitution-lint');
27
+ const { parseJUnitResults, checkAcTestPass } = require('../lib/ac-test-gate');
28
+ const { lintBusinessQuiz, validateModuleCardStructure } = require('../lib/module-card-lint');
29
+ const { readFlowState } = require('../lib/yaml-parser');
30
+ const { checkGovernance } = require('../lib/governance');
31
+
32
+ // Current artifact-schema generation. New features stamp `schema_version` in
33
+ // .flow-state.yaml. A feature stamped older than this (or unstamped) is "legacy":
34
+ // its NEW-schema warnings (TDD/AC-ID/DDL/Tech-Decisions/annotation-completeness/
35
+ // plan↔tasks…) are shown as advisory and do NOT gate `--strict`, so bumping the
36
+ // schema doesn't break in-flight work created with an older template.
37
+ const SCHEMA_VERSION = '2.0';
38
+
39
+ /** True if a feature's stamped schema version is older than current (or missing). */
40
+ function schemaIsLegacy(featureVer, current = SCHEMA_VERSION) {
41
+ if (!featureVer) return true;
42
+ const a = parseFloat(featureVer);
43
+ if (Number.isNaN(a)) return true;
44
+ return a < parseFloat(current);
45
+ }
46
+
47
+ /** Read schema_version from a feature's .flow-state.yaml (regex — no yaml dep needed). */
48
+ function readFeatureSchemaVersion(featureDir) {
49
+ try {
50
+ const p = path.join(featureDir, '.flow-state.yaml');
51
+ if (!fs.existsSync(p)) return null;
52
+ const m = fs.readFileSync(p, 'utf8').match(/^\s*schema_version:\s*["']?([\d.]+)/m);
53
+ return m ? m[1] : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /** Read pipeline_type (full|quickfix|hotfix) from .flow-state.yaml → drives the "lite" profile. */
60
+ function readPipelineType(featureDir) {
61
+ try {
62
+ const p = path.join(featureDir, '.flow-state.yaml');
63
+ if (!fs.existsSync(p)) return null;
64
+ const m = fs.readFileSync(p, 'utf8').match(/pipeline_type:\s*["']?(\w+)/);
65
+ return m ? m[1].toLowerCase() : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ // Quickfix/hotfix = "lite" profile: small change, process-heavy nudges (Tech Decisions,
72
+ // NFR section) are relaxed so a 1–2 task fix isn't drowned in advisory warnings.
73
+ function isLiteProfile(pipelineType) {
74
+ return pipelineType === 'quickfix' || pipelineType === 'hotfix';
75
+ }
76
+
77
+ /** Optional per-project tuning: .v-flow/validate.config.json { ignore:[substr], ignoreFeatures:[name] }. */
78
+ function readValidateConfig(projectRoot) {
79
+ try {
80
+ const p = path.join(getVFlowDir(projectRoot), 'validate.config.json');
81
+ if (!fs.existsSync(p)) return { ignore: [], ignoreFeatures: [] };
82
+ const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
83
+ return {
84
+ ignore: Array.isArray(cfg.ignore) ? cfg.ignore : [],
85
+ ignoreFeatures: Array.isArray(cfg.ignoreFeatures) ? cfg.ignoreFeatures : [],
86
+ };
87
+ } catch {
88
+ return { ignore: [], ignoreFeatures: [] };
89
+ }
90
+ }
91
+
92
+ /** Drop findings whose message matches any ignore substring (artifact-level rules). Mutates result; returns #suppressed. */
93
+ function applyIgnore(result, ignore) {
94
+ if (!ignore || ignore.length === 0) return 0;
95
+ const hit = (s) => ignore.some(p => s.includes(p));
96
+ let suppressed = 0;
97
+ for (const k of ['issues', 'warnings']) {
98
+ const before = result[k].length;
99
+ result[k] = result[k].filter(m => !hit(m));
100
+ suppressed += before - result[k].length;
101
+ }
102
+ return suppressed;
103
+ }
104
+
105
+ // ════════════════════════════════════════════════════════════
106
+ // VALIDATION RULES
107
+ // ════════════════════════════════════════════════════════════
108
+
109
+ /**
110
+ * Validate spec.md structure and EARS compliance
111
+ */
112
+ function validateSpec(content, filePath) {
113
+ const issues = [];
114
+ const warnings = [];
115
+ const ok = [];
116
+
117
+ // Required sections
118
+ const requiredHeadings = [
119
+ { pattern: /^##\s+.*requirements/im, label: 'Requirements section' },
120
+ ];
121
+
122
+ for (const { pattern, label } of requiredHeadings) {
123
+ if (pattern.test(content)) {
124
+ ok.push(label);
125
+ } else {
126
+ issues.push(`Missing: ${label}`);
127
+ }
128
+ }
129
+
130
+ // Check for REQ-xxx entries
131
+ const reqMatches = content.match(/REQ-[A-Z]\d+/g) || [];
132
+ const uniqueReqs = [...new Set(reqMatches)];
133
+ if (uniqueReqs.length > 0) {
134
+ ok.push(`${uniqueReqs.length} requirement(s) found: ${uniqueReqs.join(', ')}`);
135
+ } else {
136
+ issues.push('No REQ-xxx identifiers found — spec should use EARS notation');
137
+ }
138
+
139
+ // EARS notation check
140
+ const earsTypes = {
141
+ 'REQ-U': 'Ubiquitous',
142
+ 'REQ-E': 'Event-driven',
143
+ 'REQ-S': 'State-driven',
144
+ 'REQ-O': 'Optional',
145
+ };
146
+ const usedTypes = [];
147
+ for (const [prefix, label] of Object.entries(earsTypes)) {
148
+ if (uniqueReqs.some(r => r.startsWith(prefix))) {
149
+ usedTypes.push(label);
150
+ }
151
+ }
152
+ if (usedTypes.length > 0) {
153
+ ok.push(`EARS types used: ${usedTypes.join(', ')}`);
154
+ } else if (uniqueReqs.length > 0) {
155
+ warnings.push('No EARS type prefixes detected (REQ-U/E/S/O) — consider using EARS notation');
156
+ }
157
+
158
+ // Acceptance Criteria check
159
+ const acPattern = /acceptance\s*criteria|tiêu\s*chí\s*chấp\s*nhận/i;
160
+ if (acPattern.test(content)) {
161
+ ok.push('Acceptance Criteria section found');
162
+ } else {
163
+ warnings.push('No Acceptance Criteria section detected — REQs should have ACs');
164
+ }
165
+
166
+ // Acceptance Criterion IDs (AC-NN) — enable AC↔task coverage cross-check
167
+ const uniqueAcs = [...new Set(content.match(/\bAC-\d+\b/g) || [])];
168
+ if (uniqueAcs.length > 0) {
169
+ ok.push(`${uniqueAcs.length} acceptance criterion ID(s) found (AC-NN)`);
170
+ } else if (acPattern.test(content)) {
171
+ warnings.push('Acceptance Criteria nên đánh mã AC-NN — để task "Done khi" tham chiếu và cross-check coverage');
172
+ }
173
+
174
+ // Out of Scope check
175
+ const oosPattern = /out\s*of\s*scope|ngoài\s*phạm\s*vi|không\s*bao\s*gồm/i;
176
+ if (oosPattern.test(content)) {
177
+ ok.push('Out of Scope section found');
178
+ } else {
179
+ warnings.push('Consider adding Out of Scope section to prevent scope creep');
180
+ }
181
+
182
+ // Use Case Specification check
183
+ const ucMatches = content.match(/UC-\d+/g) || [];
184
+ const uniqueUcs = [...new Set(ucMatches)];
185
+ if (uniqueUcs.length > 0) {
186
+ ok.push(`${uniqueUcs.length} use case(s) found: ${uniqueUcs.join(', ')}`);
187
+
188
+ // Each detailed use case should describe preconditions, postconditions, and screens
189
+ const ucDetailChecks = [
190
+ { pattern: /điều\s*kiện\s*trước|precondition/i, label: 'Preconditions (Điều kiện trước)' },
191
+ { pattern: /điều\s*kiện\s*sau|postcondition/i, label: 'Postconditions (Điều kiện sau)' },
192
+ { pattern: /hành\s*động\s*của\s*tác\s*nhân|phản\s*ứng\s*của\s*hệ\s*thống/i, label: 'Main flow (Hành động tác nhân ↔ Phản ứng hệ thống)' },
193
+ { pattern: /luồng\s*ngoại\s*lệ|exception|alternative\s*flow/i, label: 'Exception flows (Luồng ngoại lệ)' },
194
+ { pattern: /yêu\s*cầu\s*đặc\s*biệt|special\s*requirement/i, label: 'Special requirements (Yêu cầu đặc biệt)' },
195
+ { pattern: /mô\s*tả\s*màn\s*hình|screen\s*\/?\s*ui/i, label: 'Screen description (Mô tả màn hình)' },
196
+ ];
197
+ for (const { pattern, label } of ucDetailChecks) {
198
+ if (!pattern.test(content)) {
199
+ warnings.push(`Use case spec missing: ${label}`);
200
+ }
201
+ }
202
+ } else {
203
+ warnings.push('No UC-xx identifiers found — spec should detail use cases (Đặc tả Use Case chi tiết)');
204
+ }
205
+
206
+ // Surface whether the spec declares per-UC NFR ("Yêu cầu đặc biệt") so we can
207
+ // later cross-check that the plan carries an NFR section (else NFR rots silently).
208
+ const hasSpecialReqs = /yêu\s*cầu\s*đặc\s*biệt|special\s*requirement/i.test(content);
209
+
210
+ return { issues, warnings, ok, reqCount: uniqueReqs.length, reqs: uniqueReqs, ucCount: uniqueUcs.length, ucs: uniqueUcs, hasSpecialReqs, acs: uniqueAcs };
211
+ }
212
+
213
+ /**
214
+ * Validate plan.md structure
215
+ */
216
+ function validatePlan(content, filePath, opts = {}) {
217
+ const issues = [];
218
+ const warnings = [];
219
+ const ok = [];
220
+ const lite = Boolean(opts.lite); // quickfix/hotfix: relax process-heavy nudges
221
+
222
+ // Check for REQ references (traceability)
223
+ const reqRefs = content.match(/REQ-[A-Z]\d+/g) || [];
224
+ if (reqRefs.length > 0) {
225
+ ok.push(`${new Set(reqRefs).size} REQ reference(s) found — traceability exists`);
226
+ } else {
227
+ issues.push('No REQ-xxx references — plan must trace back to spec requirements');
228
+ }
229
+
230
+ // Check for architecture section
231
+ if (/architecture|kiến\s*trúc|component|layer/i.test(content)) {
232
+ ok.push('Architecture/component section detected');
233
+ } else {
234
+ warnings.push('No architecture section detected');
235
+ }
236
+
237
+ // Check for file structure section
238
+ if (/file\s*structure|file\s*changes|cấu\s*trúc\s*file|file\s*mới|file\s*sửa|directory|thư\s*mục/i.test(content)) {
239
+ ok.push('File structure section detected');
240
+ } else if (!lite) {
241
+ warnings.push('Consider documenting file structure in plan');
242
+ }
243
+
244
+ // Use case traceability
245
+ const ucRefs = content.match(/UC-\d+/g) || [];
246
+ if (ucRefs.length > 0) {
247
+ ok.push(`${new Set(ucRefs).size} use case reference(s) found`);
248
+ } else {
249
+ warnings.push('No UC-xx references — plan should trace to use cases in spec');
250
+ }
251
+
252
+ // UI / Screen Changes section
253
+ if (/screen\s*changes|màn\s*hình|ui\s*\//i.test(content)) {
254
+ ok.push('UI/Screen Changes section detected');
255
+ } else {
256
+ warnings.push('No UI/Screen Changes section — add §5 if feature has screens (link UC + DESIGN.md tokens)');
257
+ }
258
+
259
+ // Database migrations (when schema is touched)
260
+ const touchesDb = /\bmigration\b|ALTER\s+TABLE|CREATE\s+TABLE|\bschema\b/i.test(content);
261
+ const hasMigrationSection = /database\s*migrations|\bmigration\b/i.test(content);
262
+ if (touchesDb && !hasMigrationSection) {
263
+ warnings.push('Plan references DB/schema but has no Migrations section (§3.4: bảng/cột/rollback)');
264
+ }
265
+
266
+ // Concrete DDL — when a structured schema-change table exists (NEW/ALTER rows),
267
+ // the plan should carry real DDL (or equivalent ORM commands), not just the table,
268
+ // so the dev doesn't have to guess the statement. Presence-only (no SQL linting).
269
+ const hasSchemaChangeRow = /\|\s*(NEW|ALTER)\s*\|/i.test(content);
270
+ const hasDDL = /alter\s+table|create\s+table|create\s+index|drop\s+(table|column|index)/i.test(content);
271
+ if (hasSchemaChangeRow && !hasDDL) {
272
+ warnings.push('§3.4 có bảng schema change (NEW/ALTER) nhưng thiếu DDL thật (ALTER/CREATE…) — ghi câu lệnh up+down hoặc lệnh ORM tương đương, đừng để dev đoán');
273
+ }
274
+
275
+ // Modify-file specificity (only when a "File sửa đổi" section actually exists)
276
+ const hasModifySection = /file\s*sửa\s*đổi|files?\s*modified/i.test(content);
277
+ if (hasModifySection && !/vị\s*trí|hiện\s*trạng|symbol/i.test(content)) {
278
+ warnings.push('File sửa đổi nên ghi "Vị trí (symbol)" + "Hiện trạng → Mong muốn", không mô tả chung chung');
279
+ }
280
+
281
+ // Does the plan carry an NFR section (§10)? Used for spec↔plan NFR cross-check.
282
+ const hasNfr = /non-functional|\bnfr\b|phi\s*chức\s*năng/i.test(content);
283
+
284
+ // Tech Decisions section (WHY + alternatives) — the design rationale needs a home
285
+ const hasTechDecisions = /tech\s*decision|quyết\s*định\s*kỹ\s*thuật|alternatives|phương\s*án\s*đã/i.test(content);
286
+ if (!hasTechDecisions && !lite) {
287
+ warnings.push('Plan thiếu mục Tech Decisions (WHY + phương án đã cân nhắc/loại) — lý do thiết kế dễ thất lạc');
288
+ }
289
+
290
+ // File paths referenced (backtick path-like tokens) — for plan↔tasks consistency.
291
+ // Strip fenced code blocks (``` … ```) FIRST so mermaid/code fences don't corrupt
292
+ // inline-backtick pairing; then take single-line `code` spans. Keep dir tokens
293
+ // (ending in /) so "test/products/" can cover "test/products/x.js".
294
+ const inlineSrc = content.replace(/```[\s\S]*?```/g, '');
295
+ const planFiles = [...new Set((inlineSrc.match(/`([^`\n]+)`/g) || [])
296
+ .map(t => t.replace(/`/g, '').trim())
297
+ .filter(t => t.includes('/') && (/\.\w{1,6}$/.test(t) || t.endsWith('/'))))];
298
+
299
+ return { issues, warnings, ok, hasNfr, reqs: [...new Set(reqRefs)], files: planFiles };
300
+ }
301
+
302
+ /**
303
+ * Normalize a task id (T-001 / TASK-01 / TASK-1) → canonical key "t<number>"
304
+ * so dependency refs match headers regardless of convention/padding.
305
+ */
306
+ function normTaskId(raw) {
307
+ const m = raw.match(/(?:T|TASK)-0*(\d+)/i);
308
+ return m ? `t${m[1]}` : raw.toLowerCase();
309
+ }
310
+
311
+ /**
312
+ * Build the task dependency graph from tasks.md.
313
+ * Returns { graph: Map<displayId, Set<displayId>>, danglingRefs: Set<rawRef> }.
314
+ * Deps are read from both header annotations `[DEPENDS: T-xxx]` and a
315
+ * `**Dependency**:` / `**Depends on**:` field within each task block.
316
+ */
317
+ function parseTaskGraph(content) {
318
+ const headerRe = /^#{2,4}\s*((?:T|TASK)-\d+)\b/gim;
319
+ const heads = [];
320
+ let m;
321
+ while ((m = headerRe.exec(content)) !== null) {
322
+ heads.push({ id: m[1], norm: normTaskId(m[1]), index: m.index });
323
+ }
324
+ const idByNorm = new Map();
325
+ for (const h of heads) idByNorm.set(h.norm, h.id);
326
+
327
+ const graph = new Map();
328
+ const danglingRefs = new Set();
329
+ for (let i = 0; i < heads.length; i++) {
330
+ const start = heads[i].index;
331
+ const end = i + 1 < heads.length ? heads[i + 1].index : content.length;
332
+ const block = content.slice(start, end);
333
+ const refs = [];
334
+ let d;
335
+ const depAnno = /\[DEPENDS:\s*([^\]]+)\]/gi;
336
+ while ((d = depAnno.exec(block)) !== null) refs.push(...(d[1].match(/(?:T|TASK)-\d+/gi) || []));
337
+ const depField = /\*\*\s*(?:Dependency|Dependencies|Depends on)\s*\*\*\s*:?\s*([^\n]+)/gi;
338
+ while ((d = depField.exec(block)) !== null) refs.push(...(d[1].match(/(?:T|TASK)-\d+/gi) || []));
339
+
340
+ const deps = new Set();
341
+ for (const r of refs) {
342
+ const n = normTaskId(r);
343
+ if (n === heads[i].norm) continue; // self-reference — ignore
344
+ if (idByNorm.has(n)) deps.add(idByNorm.get(n));
345
+ else danglingRefs.add(r);
346
+ }
347
+ graph.set(heads[i].id, deps);
348
+ }
349
+ return { graph, danglingRefs };
350
+ }
351
+
352
+ /**
353
+ * Split tasks.md into per-task blocks: [{ id, header, block }].
354
+ * A block spans from its header to the next task header (or EOF).
355
+ */
356
+ function extractTaskBlocks(content) {
357
+ const headerRe = /^#{2,4}\s*((?:T|TASK)-\d+)[^\n]*/gim;
358
+ const heads = [];
359
+ let m;
360
+ while ((m = headerRe.exec(content)) !== null) {
361
+ heads.push({ id: m[1], header: m[0], index: m.index });
362
+ }
363
+ // Level-2 section boundaries (## …, not ###/####) — a task block must NOT bleed past
364
+ // one, else trailing "## Dependency Graph"/"## Execution Schedule" recaps (which
365
+ // mention [GATE]/[RISKY]/[DEPENDS]) contaminate the last task's block.
366
+ const sectionRe = /^##\s+(?!#)/gim;
367
+ const sections = [];
368
+ while ((m = sectionRe.exec(content)) !== null) sections.push(m.index);
369
+ return heads.map((h, i) => {
370
+ let end = i + 1 < heads.length ? heads[i + 1].index : content.length;
371
+ const sec = sections.find(s => s > h.index && s < end);
372
+ if (sec !== undefined) end = sec;
373
+ return { id: h.id, header: h.header, block: content.slice(h.index, end) };
374
+ });
375
+ }
376
+
377
+ /**
378
+ * Detect a cycle in the dependency graph via DFS coloring.
379
+ * Returns the cycle path (array of displayIds) or null.
380
+ */
381
+ function findTaskCycle(graph) {
382
+ const color = new Map(); // 0=white, 1=gray, 2=black
383
+ for (const k of graph.keys()) color.set(k, 0);
384
+ const stack = [];
385
+ let found = null;
386
+ function dfs(u) {
387
+ color.set(u, 1);
388
+ stack.push(u);
389
+ for (const v of graph.get(u) || []) {
390
+ if (color.get(v) === 1) {
391
+ found = stack.slice(stack.indexOf(v)).concat(v);
392
+ return true;
393
+ }
394
+ if (color.get(v) === 0 && dfs(v)) return true;
395
+ }
396
+ stack.pop();
397
+ color.set(u, 2);
398
+ return false;
399
+ }
400
+ for (const k of graph.keys()) {
401
+ if (color.get(k) === 0 && dfs(k)) break;
402
+ }
403
+ return found;
404
+ }
405
+
406
+ /**
407
+ * Validate tasks.md structure
408
+ */
409
+ function validateTasks(content, filePath) {
410
+ const issues = [];
411
+ const warnings = [];
412
+ const ok = [];
413
+
414
+ // Check for task entries (accepts both T-001 and TASK-01 conventions)
415
+ const taskMatches = content.match(/###\s*(?:T|TASK)-\d+/gi) || [];
416
+ if (taskMatches.length > 0) {
417
+ ok.push(`${taskMatches.length} task(s) found`);
418
+ } else {
419
+ issues.push('No T-xxx task entries found');
420
+ }
421
+
422
+ // Check annotations — must be EXACTLY 1 entrypoint. Count on task header lines
423
+ // only, so an Execution Schedule recap mentioning [ENTRYPOINT] doesn't inflate it.
424
+ const taskHeaderLines = content.match(/^#{2,4}\s*(?:T|TASK)-\d+[^\n]*/gim) || [];
425
+ const entrypointCount = taskHeaderLines.filter(h => /\[ENTRYPOINT\]/i.test(h)).length;
426
+ if (entrypointCount === 1) {
427
+ ok.push('[ENTRYPOINT] annotation found (exactly 1)');
428
+ } else if (entrypointCount > 1) {
429
+ issues.push(`Found ${entrypointCount} [ENTRYPOINT] annotations — task list must have exactly 1 entrypoint`);
430
+ } else if (taskMatches.length > 0) {
431
+ issues.push('Missing [ENTRYPOINT] — task list must have exactly 1 entrypoint');
432
+ }
433
+
434
+ // Check for REQ traceability
435
+ const reqRefs = content.match(/REQ-[A-Z]\d+/g) || [];
436
+ const uniqueReqs = [...new Set(reqRefs)];
437
+ // Acceptance Criterion references (AC-NN) — for spec↔task AC coverage cross-check
438
+ const acRefs = [...new Set(content.match(/\bAC-\d+\b/g) || [])];
439
+ if (uniqueReqs.length > 0) {
440
+ ok.push('REQ traceability found in tasks');
441
+ } else if (taskMatches.length > 0) {
442
+ issues.push('Tasks missing REQ-xxx traceability — each task must trace to a requirement');
443
+ }
444
+
445
+ // Check for dependency declarations
446
+ const hasDeps = /\[DEPENDS/i.test(content);
447
+ if (hasDeps) {
448
+ ok.push('Dependency declarations found');
449
+ } else if (taskMatches.length > 2) {
450
+ warnings.push('No [DEPENDS] annotations — consider declaring task dependencies');
451
+ }
452
+
453
+ // Dependency graph integrity — cycles + dangling references
454
+ const { graph, danglingRefs } = parseTaskGraph(content);
455
+ const cycle = findTaskCycle(graph);
456
+ if (cycle) {
457
+ issues.push(`Circular dependency detected: ${cycle.join(' → ')}`);
458
+ } else if (hasDeps) {
459
+ ok.push('No circular dependencies');
460
+ }
461
+ if (danglingRefs.size > 0) {
462
+ warnings.push(`[DEPENDS] references unknown task(s): ${[...danglingRefs].join(', ')}`);
463
+ }
464
+
465
+ // CLARIFY — tasks needing human clarification before implementation
466
+ const clarifyCount = (content.match(/\[CLARIFY\]/gi) || []).length;
467
+ if (clarifyCount > 0) {
468
+ warnings.push(`${clarifyCount} task(s) gắn [CLARIFY] — cần làm rõ với human trước khi implement`);
469
+ }
470
+
471
+ // Task Type tags (change surface)
472
+ const typeTags = content.match(/\[(UI|API|DB|LOGIC|REFACTOR|TEST)\]/gi) || [];
473
+ if (typeTags.length > 0) {
474
+ const kinds = new Set(typeTags.map(t => t.toUpperCase()));
475
+ ok.push(`Task Type tags found (${kinds.size} type(s))`);
476
+ } else if (taskMatches.length > 0) {
477
+ warnings.push('No Task Type tags ([UI]/[API]/[DB]/[LOGIC]/[REFACTOR]/[TEST]) — tag each task by change surface');
478
+ }
479
+
480
+ // Change specificity (symbol + before→after) — only when tasks involve modifying existing code
481
+ const looksLikeModify = /\b(sửa|modify|update|refactor|ALTER)\b/i.test(content);
482
+ if (taskMatches.length > 0 && looksLikeModify && !/vị\s*trí|hiện\s*trạng|symbol/i.test(content)) {
483
+ warnings.push('Tasks lack change detail — add "Vị trí (symbol)" + "Hiện trạng → Mong muốn" for modify tasks');
484
+ }
485
+
486
+ // Use case traceability
487
+ const ucRefs = content.match(/UC-\d+/g) || [];
488
+ if (ucRefs.length > 0) {
489
+ ok.push(`Tasks trace to ${new Set(ucRefs).size} use case(s)`);
490
+ } else if (taskMatches.length > 0) {
491
+ warnings.push('Tasks missing UC-xx mapping — link each task to a use case');
492
+ }
493
+
494
+ // ── Per-task completeness (warnings) — each task block must be self-sufficient
495
+ // so Implement Agent never has to ask back. Kept as warnings (not issues) so a
496
+ // slightly incomplete task doesn't hard-fail the pipeline.
497
+ const blocks = extractTaskBlocks(content);
498
+ const TYPE_RE = /\[(UI|API|DB|LOGIC|REFACTOR|TEST)\]/i;
499
+ const CODE_TYPE_RE = /\[(UI|API|DB|LOGIC)\]/i; // code-touching tasks should drive TDD
500
+ const missing = { type: [], complexity: [], doneWhen: [], tdd: [], req: [], clarify: [], gate: [], risky: [] };
501
+ const tier = { S: 0, M: 0, L: 0 };
502
+ const taskFiles = new Set();
503
+ let points = 0;
504
+ for (const b of blocks) {
505
+ if (!TYPE_RE.test(b.block)) missing.type.push(b.id);
506
+
507
+ // Annotation-completeness — enforce the rules the skills declare
508
+ if (/\[CLARIFY\]/i.test(b.block) && !/câu\s*hỏi\s*mở|open\s*question/i.test(b.block)) missing.clarify.push(b.id);
509
+ if (/\[GATE\]/i.test(b.block) && !/gate\s*condition|điều\s*kiện.*gate/i.test(b.block)) missing.gate.push(b.id);
510
+ if (/\[RISKY\]/i.test(b.block) && !/fallback|\brisk\b|rủi\s*ro/i.test(b.block)) missing.risky.push(b.id);
511
+
512
+ // Collect declared files (for plan↔tasks consistency)
513
+ const ff = b.block.match(/\*\*\s*Files?\s*\*\*\s*:?\s*([^\n]+)/i);
514
+ if (ff) {
515
+ for (const raw of (ff[1].match(/`([^`]+)`/g) || [])) {
516
+ const k = raw.replace(/`/g, '').trim();
517
+ if (k) taskFiles.add(k);
518
+ }
519
+ }
520
+
521
+ const cm = b.block.match(/\*\*\s*Complexity\s*\*\*\s*:?\s*([SML])\b/i);
522
+ if (cm) {
523
+ const t = cm[1].toUpperCase();
524
+ tier[t] += 1;
525
+ points += t === 'S' ? 1 : t === 'M' ? 2 : 4;
526
+ } else {
527
+ missing.complexity.push(b.id);
528
+ }
529
+
530
+ if (!/done\s*khi|done\s*when/i.test(b.block)) missing.doneWhen.push(b.id);
531
+ if (!/REQ-[A-Z]\d+/.test(b.block)) missing.req.push(b.id);
532
+ // TDD checklist expected only on code-touching tasks (not pure [TEST]/[REFACTOR])
533
+ if (CODE_TYPE_RE.test(b.block) && !/\bTDD\b/i.test(b.block)) missing.tdd.push(b.id);
534
+ }
535
+ if (blocks.length > 0) {
536
+ if (missing.type.length) warnings.push(`Task thiếu Task Type: ${missing.type.join(', ')}`);
537
+ if (missing.complexity.length) warnings.push(`Task thiếu Complexity (S/M/L): ${missing.complexity.join(', ')}`);
538
+ if (missing.doneWhen.length) warnings.push(`Task thiếu "Done khi" (bám AC spec §4, không chỉ test pass): ${missing.doneWhen.join(', ')}`);
539
+ if (missing.tdd.length) warnings.push(`Task code thiếu block TDD (Red→Green): ${missing.tdd.join(', ')}`);
540
+ if (uniqueReqs.length > 0 && missing.req.length) warnings.push(`Task thiếu REQ mapping: ${missing.req.join(', ')}`);
541
+ if (missing.clarify.length) warnings.push(`[CLARIFY] thiếu "Câu hỏi mở": ${missing.clarify.join(', ')}`);
542
+ if (missing.gate.length) warnings.push(`[GATE] thiếu "Gate condition": ${missing.gate.join(', ')}`);
543
+ if (missing.risky.length) warnings.push(`[RISKY] thiếu "Fallback/Risk": ${missing.risky.join(', ')}`);
544
+
545
+ // Sizing — quy đổi S=1/M=2/L=4 để biết khi nào nên tách milestone
546
+ if (points > 0) {
547
+ ok.push(`Sizing: Σ ${points} điểm (S:${tier.S} · M:${tier.M} · L:${tier.L})`);
548
+ if (points > 24 || blocks.length > 20) {
549
+ warnings.push(`Quy mô lớn (Σ ${points} điểm / ${blocks.length} task) — cân nhắc tách thành nhiều phase/milestone`);
550
+ }
551
+ }
552
+ }
553
+
554
+ // ── Fake [PARALLEL] — two parallel tasks editing the same file must be sequential
555
+ const parallelBlocks = blocks.filter(b => /\[PARALLEL\]/i.test(b.block));
556
+ const fileToTasks = new Map();
557
+ for (const b of parallelBlocks) {
558
+ const fileField = b.block.match(/\*\*\s*Files?\s*\*\*\s*:?\s*([^\n]+)/i);
559
+ if (!fileField) continue;
560
+ const paths = fileField[1].match(/`([^`]+)`/g) || [];
561
+ for (const raw of paths) {
562
+ const key = raw.replace(/`/g, '').trim();
563
+ if (!key) continue;
564
+ if (!fileToTasks.has(key)) fileToTasks.set(key, new Set());
565
+ fileToTasks.get(key).add(b.id);
566
+ }
567
+ }
568
+ for (const [file, ids] of fileToTasks) {
569
+ if (ids.size > 1) {
570
+ warnings.push(`Fake [PARALLEL]: ${[...ids].join(', ')} cùng sửa \`${file}\` — nên chạy tuần tự`);
571
+ }
572
+ }
573
+
574
+ return { issues, warnings, ok, taskCount: taskMatches.length, reqs: uniqueReqs, acs: acRefs, files: [...taskFiles] };
575
+ }
576
+
577
+ /**
578
+ * Validate test-cases.md structure
579
+ */
580
+ function validateTestCases(content, filePath) {
581
+ const issues = [];
582
+ const warnings = [];
583
+ const ok = [];
584
+
585
+ // Check for TC entries
586
+ const tcMatches = content.match(/###\s*TC-/g) || [];
587
+ if (tcMatches.length > 0) {
588
+ ok.push(`${tcMatches.length} test case(s) found`);
589
+ } else {
590
+ issues.push('No TC-xxx test case entries found');
591
+ }
592
+
593
+ // Coverage matrix — chấp nhận biến thể heading: "Coverage Matrix", "Test Matrix",
594
+ // hoặc tiếng Việt "Ma trận truy vết" (khớp test-cases-template.md §1).
595
+ if (/coverage\s*matrix|test\s*matrix|ma\s*trận\s*truy\s*vết/i.test(content)) {
596
+ ok.push('Coverage matrix found');
597
+ } else if (tcMatches.length > 0) {
598
+ warnings.push('No Coverage Matrix section — add one at the top of test-cases.md');
599
+ }
600
+
601
+ // Check for REQ traceability
602
+ const reqRefs = content.match(/REQ-[A-Z]\d+/g) || [];
603
+ const uniqueReqRefs = [...new Set(reqRefs)];
604
+ if (uniqueReqRefs.length > 0) {
605
+ ok.push(`Test cases trace to ${uniqueReqRefs.length} unique REQ(s)`);
606
+ } else if (tcMatches.length > 0) {
607
+ issues.push('Test cases missing REQ-xxx traceability');
608
+ }
609
+
610
+ // Acceptance Criterion (AC-NN) traceability — enable AC↔TC coverage cross-check.
611
+ // Structural only: presence of the AC id, NOT whether the TC truly verifies it (Master Check owns that).
612
+ const acRefs = [...new Set(content.match(/\bAC-\d+\b/g) || [])];
613
+ if (acRefs.length > 0) {
614
+ ok.push(`Test cases trace to ${acRefs.length} unique AC-NN`);
615
+ } else if (tcMatches.length > 0) {
616
+ warnings.push('Test cases thiếu mã AC-NN — thêm cột/field AC để cross-check mỗi AC (spec §4) có ≥1 TC');
617
+ }
618
+
619
+ // Priority check
620
+ if (/P1|P2|P3|priority/i.test(content)) {
621
+ ok.push('Priority assignments found');
622
+ } else if (tcMatches.length > 0) {
623
+ warnings.push('No priority assignments (P1/P2/P3) found on test cases');
624
+ }
625
+
626
+ return { issues, warnings, ok, tcCount: tcMatches.length, reqs: uniqueReqRefs, acs: acRefs };
627
+ }
628
+
629
+ /**
630
+ * Cross-validate spec REQs vs test-cases coverage
631
+ */
632
+ function crossValidateReqCoverage(specResult, testResult) {
633
+ if (!specResult || !testResult) return null;
634
+
635
+ const specReqs = specResult.reqs || [];
636
+ const testReqs = testResult.reqs || [];
637
+
638
+ const covered = specReqs.filter(r => testReqs.includes(r));
639
+ const uncovered = specReqs.filter(r => !testReqs.includes(r));
640
+
641
+ return {
642
+ total: specReqs.length,
643
+ covered: covered.length,
644
+ uncovered,
645
+ percentage: specReqs.length > 0 ? Math.round((covered.length / specReqs.length) * 100) : 0,
646
+ };
647
+ }
648
+
649
+ /**
650
+ * Cross-validate spec REQs vs tasks coverage — every REQ must have ≥1 task.
651
+ */
652
+ function crossValidateTaskCoverage(specResult, tasksResult) {
653
+ if (!specResult || !tasksResult) return null;
654
+
655
+ const specReqs = specResult.reqs || [];
656
+ const taskReqs = tasksResult.reqs || [];
657
+
658
+ const covered = specReqs.filter(r => taskReqs.includes(r));
659
+ const uncovered = specReqs.filter(r => !taskReqs.includes(r));
660
+
661
+ return {
662
+ total: specReqs.length,
663
+ covered: covered.length,
664
+ uncovered,
665
+ percentage: specReqs.length > 0 ? Math.round((covered.length / specReqs.length) * 100) : 0,
666
+ };
667
+ }
668
+
669
+ /**
670
+ * Cross-validate: spec declares NFR ("Yêu cầu đặc biệt") but plan has no NFR section.
671
+ * Returns { warn: boolean }.
672
+ */
673
+ function crossValidateNfr(specResult, planResult) {
674
+ if (!specResult || !planResult) return null;
675
+ return { warn: Boolean(specResult.hasSpecialReqs) && !planResult.hasNfr };
676
+ }
677
+
678
+ /**
679
+ * Cross-validate: every spec acceptance criterion (AC-NN) is referenced by ≥1 task.
680
+ * Returns null when the spec defines no AC IDs (feature is opt-in, no false alarms).
681
+ */
682
+ function crossValidateAcCoverage(specResult, tasksResult) {
683
+ if (!specResult || !tasksResult) return null;
684
+ const specAcs = specResult.acs || [];
685
+ if (specAcs.length === 0) return null; // AC IDs optional — only check when present
686
+ const taskAcs = tasksResult.acs || [];
687
+ const covered = specAcs.filter(a => taskAcs.includes(a));
688
+ const uncovered = specAcs.filter(a => !taskAcs.includes(a));
689
+ return {
690
+ total: specAcs.length,
691
+ covered: covered.length,
692
+ uncovered,
693
+ percentage: Math.round((covered.length / specAcs.length) * 100),
694
+ };
695
+ }
696
+
697
+ /**
698
+ * Cross-validate: every spec acceptance criterion (AC-NN) is verified by ≥1 test case.
699
+ * Mirror of crossValidateAcCoverage but for test-cases.md. Opt-in: returns null when the
700
+ * spec defines no AC IDs (no false alarms on AC-less specs).
701
+ */
702
+ function crossValidateTestAcCoverage(specResult, testResult) {
703
+ if (!specResult || !testResult) return null;
704
+ const specAcs = specResult.acs || [];
705
+ if (specAcs.length === 0) return null; // AC IDs optional — only check when present
706
+ const testAcs = testResult.acs || [];
707
+ const covered = specAcs.filter(a => testAcs.includes(a));
708
+ const uncovered = specAcs.filter(a => !testAcs.includes(a));
709
+ return {
710
+ total: specAcs.length,
711
+ covered: covered.length,
712
+ uncovered,
713
+ percentage: Math.round((covered.length / specAcs.length) * 100),
714
+ };
715
+ }
716
+
717
+ /**
718
+ * Cross-validate: every spec REQ appears in plan.md (spec→plan coverage).
719
+ * Returns null when spec has no REQs.
720
+ */
721
+ function crossValidatePlanReqCoverage(specResult, planResult) {
722
+ if (!specResult || !planResult) return null;
723
+ const specReqs = specResult.reqs || [];
724
+ if (specReqs.length === 0) return null;
725
+ const planReqs = planResult.reqs || [];
726
+ const uncovered = specReqs.filter(r => !planReqs.includes(r));
727
+ return {
728
+ total: specReqs.length,
729
+ covered: specReqs.length - uncovered.length,
730
+ uncovered,
731
+ percentage: Math.round(((specReqs.length - uncovered.length) / specReqs.length) * 100),
732
+ };
733
+ }
734
+
735
+ /**
736
+ * Dangling refs: tasks cite a REQ/AC id that does NOT exist in the spec
737
+ * (typo / copy-paste drift). Symmetric to the dangling-[DEPENDS] check.
738
+ */
739
+ function crossValidateDanglingRefs(specResult, tasksResult) {
740
+ if (!specResult || !tasksResult) return null;
741
+ const specReqs = new Set(specResult.reqs || []);
742
+ const specAcs = new Set(specResult.acs || []);
743
+ return {
744
+ danglingReqs: (tasksResult.reqs || []).filter(r => specReqs.size > 0 && !specReqs.has(r)),
745
+ danglingAcs: (tasksResult.acs || []).filter(a => specAcs.size > 0 && !specAcs.has(a)),
746
+ };
747
+ }
748
+
749
+ /**
750
+ * Plan ↔ Tasks consistency — the two artifacts most likely to drift from each other.
751
+ * Flags REQ in plan not covered by any task, and task files not planned in plan §6.
752
+ * A plan dir token ("a/b/") covers any file beneath it.
753
+ */
754
+ function crossValidatePlanTasks(planResult, tasksResult) {
755
+ if (!planResult || !tasksResult) return null;
756
+ const planReqs = new Set(planResult.reqs || []);
757
+ const taskReqs = new Set(tasksResult.reqs || []);
758
+ const planFiles = planResult.files || [];
759
+ const filesNotInPlan = (tasksResult.files || []).filter(f => {
760
+ if (planFiles.length === 0) return false; // plan has no parseable paths → skip
761
+ if (planFiles.includes(f)) return false;
762
+ if (planFiles.some(p => p.endsWith('/') && f.startsWith(p))) return false;
763
+ return true;
764
+ });
765
+ return {
766
+ reqOnlyInPlan: [...planReqs].filter(r => !taskReqs.has(r)),
767
+ reqOnlyInTasks: [...taskReqs].filter(r => !planReqs.has(r)),
768
+ filesNotInPlan,
769
+ };
770
+ }
771
+
772
+ // ════════════════════════════════════════════════════════════
773
+ // MAIN COMMAND
774
+ // ════════════════════════════════════════════════════════════
775
+
776
+ module.exports = function (program) {
777
+ program
778
+ .command('validate')
779
+ .description('Validate V-Flow artifact structure and quality')
780
+ .argument('[feature]', 'Feature name to validate (default: all features)')
781
+ .option('--spec-only', 'Only validate spec.md')
782
+ .option('--strict', 'Treat warnings as errors — dùng cho CI / cổng trước-implement để chặn lỗi traceability (REQ/AC/task coverage)')
783
+ .option('--quiet', 'Chỉ hiện issue + warning (ẩn dòng "check passed")')
784
+ .option('--constitution', 'Validate source code against constitution.md [LINT:xxx] rules')
785
+ .option('--src <dir>', 'Source directory for constitution lint (default: src/)')
786
+ .option('--test-results <file>', 'JUnit XML — bật AC-PASS gate: mỗi AC-NN (spec) phải có ≥1 test PASS tham chiếu')
787
+ .option('--module-cards [module]', 'Lint module card (.v-flow/modules/) — cấu trúc + ngôn ngữ business-quiz (cấm dính code). Bỏ trống = tất cả; truyền tên = chỉ module đó (fallback domain-<tên>)')
788
+ .action(withErrorHandler(async (featureName, options) => {
789
+ const t = createT();
790
+ const projectRoot = process.cwd();
791
+ requireInit(projectRoot, isInitialized);
792
+
793
+ const cfg = readValidateConfig(projectRoot);
794
+ let suppressedTotal = 0;
795
+
796
+ console.log(chalk.cyan.bold('\n🔍 V Flow — Validate\n'));
797
+
798
+ // ════════════════════════════════════════════════════════
799
+ // Constitution Lint (if --constitution flag)
800
+ // ════════════════════════════════════════════════════════
801
+ if (options.constitution) {
802
+ const constitutionPath = path.join(getVFlowDir(projectRoot), 'constitution.md');
803
+ if (!fs.existsSync(constitutionPath)) {
804
+ printError('constitution.md not found — run v-flow init first');
805
+ process.exit(1);
806
+ }
807
+
808
+ const content = await fs.readFile(constitutionPath, 'utf8');
809
+ console.log(chalk.white.bold('📜 Constitution Lint\n'));
810
+ console.log(chalk.dim(' Parsing constitution.md for [LINT:xxx] rules...\n'));
811
+
812
+ const { rules, parseErrors } = parseConstitution(content);
813
+
814
+ // Report parse errors
815
+ for (const err of parseErrors) {
816
+ printWarn(err);
817
+ }
818
+
819
+ if (rules.length === 0) {
820
+ console.log(chalk.dim(' No [LINT:xxx] rules found in constitution.md.'));
821
+ console.log(chalk.dim(' Add rules like: - [LINT:naming] Files must use kebab-case'));
822
+ console.log(chalk.dim(' See: learn/09-constitution-lint.md\n'));
823
+ if (!featureName && !options.specOnly) return;
824
+ } else {
825
+ console.log(chalk.white(` Found ${rules.length} lintable rule(s):\n`));
826
+ for (const rule of rules) {
827
+ const sevIcon = rule.severity === 'critical' ? '🔴' : rule.severity === 'error' ? '🟡' : '⚪';
828
+ console.log(chalk.dim(` ${sevIcon} [${rule.id}] [${rule.category}] ${rule.text}`));
829
+ }
830
+ console.log('');
831
+
832
+ // Run checks
833
+ console.log(chalk.dim(' Scanning project...\n'));
834
+ const lintOptions = {
835
+ srcDir: options.src ? path.resolve(projectRoot, options.src) : undefined,
836
+ verbose: true,
837
+ };
838
+ const { violations, stats } = await runConstitutionLint(rules, projectRoot, lintOptions);
839
+
840
+ // Report violations
841
+ if (violations.length === 0) {
842
+ printOk(`All ${stats.total} constitution rules passed`);
843
+ } else {
844
+ for (const v of violations) {
845
+ console.log(` ${formatViolation(v)}`);
846
+ }
847
+ console.log('');
848
+ }
849
+
850
+ // Summary
851
+ console.log(chalk.cyan.bold(' ── Constitution Lint Summary ──\n'));
852
+ if (stats.critical > 0) {
853
+ console.log(chalk.red(` 🔴 ${stats.critical} critical violation(s)`));
854
+ }
855
+ if (stats.errors > 0) {
856
+ console.log(chalk.red(` ❌ ${stats.errors} error(s)`));
857
+ }
858
+ if (stats.warnings > 0) {
859
+ console.log(chalk.yellow(` ⚠️ ${stats.warnings} warning(s)`));
860
+ }
861
+ console.log(chalk.green(` ✅ ${stats.passed}/${stats.total} rule(s) passed`));
862
+ const compliancePercent = stats.total > 0 ? Math.round((stats.passed / stats.total) * 100) : 100;
863
+ console.log(chalk.dim(` 📊 Constitution compliance: ${compliancePercent}%\n`));
864
+
865
+ if (stats.critical > 0) {
866
+ printError('Critical constitution violations found — CI should fail');
867
+ if (!featureName && !options.specOnly) {
868
+ process.exit(1);
869
+ }
870
+ }
871
+ }
872
+
873
+ // If only --constitution was requested (no feature name), stop here
874
+ if (!featureName && !options.specOnly) return;
875
+ console.log(''); // separator
876
+ }
877
+
878
+ // ════════════════════════════════════════════════════════
879
+ // Module Card Lint (if --module-cards flag)
880
+ // Structural + business-language lint for Understand-tier cards.
881
+ // Self-contained like the constitution block; returns early when run
882
+ // standalone (no feature name).
883
+ // ════════════════════════════════════════════════════════
884
+ if (options.moduleCards) {
885
+ const modulesDir = path.join(getVFlowDir(projectRoot), 'modules');
886
+ console.log(chalk.white.bold('🗂️ Module Card Lint\n'));
887
+
888
+ const allCardDirs = fs.existsSync(modulesDir)
889
+ ? (await fs.readdir(modulesDir, { withFileTypes: true }))
890
+ .filter((d) => d.isDirectory())
891
+ .map((d) => d.name)
892
+ .sort()
893
+ : [];
894
+
895
+ // `--module-cards <name>` lints just that card (fallback to domain-<name>,
896
+ // same resolution as score-card / dev-quiz); bare `--module-cards` lints all.
897
+ const targetModule = typeof options.moduleCards === 'string' ? options.moduleCards : null;
898
+ let cardDirs = allCardDirs;
899
+ if (targetModule) {
900
+ cardDirs = allCardDirs.filter((c) => c === targetModule || c === `domain-${targetModule}`);
901
+ if (cardDirs.length === 0) {
902
+ printError(`Không tìm thấy module card "${targetModule}" trong .v-flow/modules/. Có: ${allCardDirs.length ? allCardDirs.join(', ') : '(chưa có card nào)'}`);
903
+ process.exit(1);
904
+ }
905
+ }
906
+
907
+ if (cardDirs.length === 0) {
908
+ console.log(chalk.dim(' Chưa có module card nào (.v-flow/modules/) — chạy /v.understand trước.\n'));
909
+ } else {
910
+ let cardIssues = 0;
911
+ let cardWarnings = 0;
912
+
913
+ for (const card of cardDirs) {
914
+ const cardPath = path.join(modulesDir, card);
915
+ const readCardFile = (f) => {
916
+ const p = path.join(cardPath, f);
917
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null;
918
+ };
919
+
920
+ const structResult = validateModuleCardStructure(card, readCardFile);
921
+ printArtifactResults(card, structResult, options.quiet);
922
+ cardIssues += structResult.issues.length;
923
+ cardWarnings += structResult.warnings.length;
924
+
925
+ const quizContent = readCardFile('business-quiz.md');
926
+ if (quizContent !== null && quizContent !== undefined) {
927
+ const quizResult = lintBusinessQuiz(quizContent);
928
+ printArtifactResults(`${card}/business-quiz.md`, quizResult, options.quiet);
929
+ cardIssues += quizResult.issues.length;
930
+ cardWarnings += quizResult.warnings.length;
931
+ }
932
+ }
933
+
934
+ console.log(chalk.cyan.bold('\n ── Module Card Lint Summary ──\n'));
935
+ console.log(chalk.dim(` 📦 ${cardDirs.length} module card`));
936
+ if (cardIssues > 0) console.log(chalk.red(` ❌ ${cardIssues} issue`));
937
+ if (cardWarnings > 0) console.log(chalk.yellow(` ⚠️ ${cardWarnings} warning`));
938
+ if (cardIssues === 0 && cardWarnings === 0) console.log(chalk.green(' ✅ Tất cả module card đạt'));
939
+ console.log('');
940
+
941
+ const cardFails = cardIssues > 0 || (options.strict && cardWarnings > 0);
942
+ if (cardFails && !featureName && !options.specOnly) {
943
+ printError(options.strict
944
+ ? 'Module card có issue/warning — --strict bật → fail.'
945
+ : 'Module card có issue → fail.');
946
+ process.exit(1);
947
+ }
948
+ }
949
+
950
+ if (!featureName && !options.specOnly) return;
951
+ console.log('');
952
+ }
953
+
954
+ // Parse JUnit test results once (opt-in AC-PASS gate). An objective runner
955
+ // fact — not semantic judgment — so it belongs in the deterministic validator.
956
+ let junitCases = null;
957
+ if (options.testResults) {
958
+ const trPath = path.resolve(projectRoot, options.testResults);
959
+ if (!fs.existsSync(trPath)) {
960
+ printError(`--test-results: file không tồn tại: ${options.testResults}`);
961
+ process.exit(1);
962
+ }
963
+ junitCases = parseJUnitResults(await fs.readFile(trPath, 'utf8'));
964
+ console.log(chalk.dim(` 🧪 Đã đọc ${junitCases.length} testcase từ ${options.testResults}\n`));
965
+ }
966
+
967
+ const features = featureName
968
+ ? [featureName]
969
+ : await listFeatures(projectRoot);
970
+
971
+ if (features.length === 0) {
972
+ console.log(chalk.dim(' No features found. Create one with: v-flow feature create <name>'));
973
+ return;
974
+ }
975
+
976
+ let totalIssues = 0;
977
+ let totalWarnings = 0;
978
+ let totalOk = 0;
979
+ let legacyWarnings = 0;
980
+
981
+ for (const feature of features) {
982
+ if (cfg.ignoreFeatures.includes(feature)) {
983
+ console.log(chalk.dim(`📋 Feature: ${feature} — bỏ qua (ignoreFeatures)\n`));
984
+ continue;
985
+ }
986
+ console.log(chalk.white.bold(`📋 Feature: ${feature}\n`));
987
+
988
+ const featureDir = path.join(getVFlowDir(projectRoot), 'features', feature);
989
+ if (!fs.existsSync(featureDir)) {
990
+ printError(`Feature directory not found: ${feature}`);
991
+ totalIssues++;
992
+ continue;
993
+ }
994
+
995
+ // Schema-version gate: legacy features' new-schema warnings stay advisory.
996
+ const warnAtFeatureStart = totalWarnings;
997
+ const featureSchemaVer = readFeatureSchemaVersion(featureDir);
998
+ const isLegacy = schemaIsLegacy(featureSchemaVer);
999
+ const isLite = isLiteProfile(readPipelineType(featureDir));
1000
+
1001
+ let specResult = null;
1002
+ let planResult = null;
1003
+ let tasksResult = null;
1004
+ let testResult = null;
1005
+
1006
+ // Validate spec.md
1007
+ const specPath = path.join(featureDir, 'spec.md');
1008
+ if (fs.existsSync(specPath)) {
1009
+ const content = await fs.readFile(specPath, 'utf8');
1010
+ specResult = validateSpec(content, specPath);
1011
+ suppressedTotal += applyIgnore(specResult, cfg.ignore);
1012
+ printArtifactResults('spec.md', specResult, options.quiet);
1013
+ totalIssues += specResult.issues.length;
1014
+ totalWarnings += specResult.warnings.length;
1015
+ totalOk += specResult.ok.length;
1016
+ } else {
1017
+ printWarn('spec.md not found — run /v.specify first');
1018
+ totalWarnings++;
1019
+ }
1020
+
1021
+ if (options.specOnly) continue;
1022
+
1023
+ // Validate plan.md
1024
+ const planPath = path.join(featureDir, 'plan.md');
1025
+ if (fs.existsSync(planPath)) {
1026
+ const content = await fs.readFile(planPath, 'utf8');
1027
+ planResult = validatePlan(content, planPath, { lite: isLite });
1028
+ suppressedTotal += applyIgnore(planResult, cfg.ignore);
1029
+ printArtifactResults('plan.md', planResult, options.quiet);
1030
+ totalIssues += planResult.issues.length;
1031
+ totalWarnings += planResult.warnings.length;
1032
+ totalOk += planResult.ok.length;
1033
+ }
1034
+
1035
+ // Validate tasks.md
1036
+ const tasksPath = path.join(featureDir, 'tasks.md');
1037
+ if (fs.existsSync(tasksPath)) {
1038
+ const content = await fs.readFile(tasksPath, 'utf8');
1039
+ tasksResult = validateTasks(content, tasksPath);
1040
+ suppressedTotal += applyIgnore(tasksResult, cfg.ignore);
1041
+ printArtifactResults('tasks.md', tasksResult, options.quiet);
1042
+ totalIssues += tasksResult.issues.length;
1043
+ totalWarnings += tasksResult.warnings.length;
1044
+ totalOk += tasksResult.ok.length;
1045
+ }
1046
+
1047
+ // Validate test-cases.md
1048
+ const testPath = path.join(featureDir, 'test-cases.md');
1049
+ if (fs.existsSync(testPath)) {
1050
+ const content = await fs.readFile(testPath, 'utf8');
1051
+ testResult = validateTestCases(content, testPath);
1052
+ suppressedTotal += applyIgnore(testResult, cfg.ignore);
1053
+ printArtifactResults('test-cases.md', testResult, options.quiet);
1054
+ totalIssues += testResult.issues.length;
1055
+ totalWarnings += testResult.warnings.length;
1056
+ totalOk += testResult.ok.length;
1057
+ }
1058
+
1059
+ // Cross-validate REQ → task coverage (every spec REQ has ≥1 task)
1060
+ const taskCoverage = crossValidateTaskCoverage(specResult, tasksResult);
1061
+ if (taskCoverage && taskCoverage.total > 0) {
1062
+ console.log(chalk.white.bold(`\n 📊 Task REQ Coverage: ${taskCoverage.covered}/${taskCoverage.total} (${taskCoverage.percentage}%)`));
1063
+ if (taskCoverage.uncovered.length > 0) {
1064
+ printWarn(`REQ chưa có task nào: ${taskCoverage.uncovered.join(', ')}`);
1065
+ totalWarnings += taskCoverage.uncovered.length;
1066
+ } else {
1067
+ printOk('100% REQ có task');
1068
+ totalOk++;
1069
+ }
1070
+ }
1071
+
1072
+ // Cross-validate AC → task coverage (every spec AC-NN has ≥1 task)
1073
+ const acCoverage = crossValidateAcCoverage(specResult, tasksResult);
1074
+ if (acCoverage) {
1075
+ console.log(chalk.white.bold(`\n 📊 AC Coverage: ${acCoverage.covered}/${acCoverage.total} (${acCoverage.percentage}%)`));
1076
+ if (acCoverage.uncovered.length > 0) {
1077
+ printWarn(`AC chưa có task nào phủ: ${acCoverage.uncovered.join(', ')}`);
1078
+ totalWarnings += acCoverage.uncovered.length;
1079
+ } else {
1080
+ printOk('100% AC có task');
1081
+ totalOk++;
1082
+ }
1083
+ }
1084
+
1085
+ // Cross-validate REQ → plan coverage (every spec REQ appears in plan.md)
1086
+ const planReqCov = crossValidatePlanReqCoverage(specResult, planResult);
1087
+ if (planReqCov && planReqCov.uncovered.length > 0) {
1088
+ printWarn(`REQ chưa xuất hiện trong plan.md: ${planReqCov.uncovered.join(', ')}`);
1089
+ totalWarnings += planReqCov.uncovered.length;
1090
+ }
1091
+
1092
+ // Dangling refs — tasks cite REQ/AC not present in the spec
1093
+ const dangling = crossValidateDanglingRefs(specResult, tasksResult);
1094
+ if (dangling && dangling.danglingReqs.length > 0) {
1095
+ printWarn(`Task tham chiếu REQ không có trong spec: ${dangling.danglingReqs.join(', ')}`);
1096
+ totalWarnings += dangling.danglingReqs.length;
1097
+ }
1098
+ if (dangling && dangling.danglingAcs.length > 0) {
1099
+ printWarn(`Task tham chiếu AC không có trong spec: ${dangling.danglingAcs.join(', ')}`);
1100
+ totalWarnings += dangling.danglingAcs.length;
1101
+ }
1102
+
1103
+ // Plan ↔ Tasks consistency — REQ drift + task file not planned
1104
+ const planTasks = crossValidatePlanTasks(planResult, tasksResult);
1105
+ if (planTasks && planTasks.reqOnlyInPlan.length > 0) {
1106
+ printWarn(`REQ có trong plan nhưng không task nào phủ: ${planTasks.reqOnlyInPlan.join(', ')}`);
1107
+ totalWarnings += planTasks.reqOnlyInPlan.length;
1108
+ }
1109
+ if (planTasks && planTasks.filesNotInPlan.length > 0) {
1110
+ printWarn(`Task đụng file không có trong plan §6 (File Changes): ${planTasks.filesNotInPlan.join(', ')}`);
1111
+ totalWarnings += planTasks.filesNotInPlan.length;
1112
+ }
1113
+
1114
+ // Cross-validate NFR: spec has special requirements but plan lacks an NFR section
1115
+ const nfr = crossValidateNfr(specResult, planResult);
1116
+ if (!isLite && nfr && nfr.warn) {
1117
+ printWarn('Spec có "Yêu cầu đặc biệt" (NFR) nhưng plan thiếu mục NFR (§10) — NFR dễ rơi rụng khi implement');
1118
+ totalWarnings++;
1119
+ }
1120
+
1121
+ // Cross-validate REQ coverage
1122
+ const coverage = crossValidateReqCoverage(specResult, testResult);
1123
+ if (coverage) {
1124
+ console.log(chalk.white.bold(`\n 📊 REQ Coverage: ${coverage.covered}/${coverage.total} (${coverage.percentage}%)`));
1125
+ if (coverage.uncovered.length > 0) {
1126
+ printWarn(`Uncovered REQs: ${coverage.uncovered.join(', ')}`);
1127
+ totalWarnings += coverage.uncovered.length;
1128
+ } else {
1129
+ printOk('100% REQ coverage in test cases');
1130
+ totalOk++;
1131
+ }
1132
+ }
1133
+
1134
+ // Cross-validate AC → test coverage (every spec AC-NN verified by ≥1 TC)
1135
+ const testAcCoverage = crossValidateTestAcCoverage(specResult, testResult);
1136
+ if (testAcCoverage) {
1137
+ console.log(chalk.white.bold(`\n 📊 AC Coverage (tests): ${testAcCoverage.covered}/${testAcCoverage.total} (${testAcCoverage.percentage}%)`));
1138
+ if (testAcCoverage.uncovered.length > 0) {
1139
+ printWarn(`AC chưa có test nào verify: ${testAcCoverage.uncovered.join(', ')}`);
1140
+ totalWarnings += testAcCoverage.uncovered.length;
1141
+ } else {
1142
+ printOk('100% AC có test');
1143
+ totalOk++;
1144
+ }
1145
+ }
1146
+
1147
+ // AC-PASS gate — every spec AC-NN backed by a PASSING test (opt-in --test-results).
1148
+ // This is the spec↔code "living-spec" check: coverage above is on-paper; this
1149
+ // reaches the real runner and blocks when an AC has no passing test behind it.
1150
+ if (junitCases && specResult && (specResult.acs || []).length > 0) {
1151
+ const gate = checkAcTestPass(specResult.acs, junitCases);
1152
+ console.log(chalk.white.bold(`\n 🚦 AC-PASS Gate: ${gate.passed.length}/${gate.total} AC có test PASS`));
1153
+ if (gate.failing.length > 0) {
1154
+ printError(`AC có test nhưng FAIL/skip — chưa thỏa: ${gate.failing.join(', ')}`);
1155
+ totalIssues += gate.failing.length; // hard issue — always blocks
1156
+ }
1157
+ if (gate.missing.length > 0) {
1158
+ printWarn(`AC chưa có test PASS nào tham chiếu (đặt tên test chứa AC-NN): ${gate.missing.join(', ')}`);
1159
+ totalWarnings += gate.missing.length; // blocks under --strict
1160
+ }
1161
+ if (gate.failing.length === 0 && gate.missing.length === 0) {
1162
+ printOk('100% AC có test PASS');
1163
+ totalOk++;
1164
+ }
1165
+ }
1166
+
1167
+ // Legacy schema: demote this feature's new-schema warnings to advisory (no --strict gate)
1168
+ if (isLegacy) {
1169
+ const featWarn = totalWarnings - warnAtFeatureStart;
1170
+ if (featWarn > 0) {
1171
+ totalWarnings -= featWarn;
1172
+ legacyWarnings += featWarn;
1173
+ console.log(chalk.dim(` 📦 Schema cũ (${featureSchemaVer || 'chưa stamp'} < ${SCHEMA_VERSION}): ${featWarn} cảnh báo schema-mới → tham khảo, KHÔNG chặn --strict. Migrate: bổ sung field thiếu rồi đặt schema_version: "${SCHEMA_VERSION}" trong .flow-state.yaml.`));
1174
+ }
1175
+ }
1176
+
1177
+ // Governance gate — risk_tier + required approvals (lightweight; --strict blocks).
1178
+ // After the legacy block so it is NOT demoted: governance applies regardless of schema age.
1179
+ const flowState = await readFlowState(featureDir);
1180
+ const gov = checkGovernance(flowState || {});
1181
+ if (gov.tier || gov.warnings.length > 0) {
1182
+ console.log(chalk.white.bold(`\n 🛡️ Governance: risk_tier = ${gov.tier || '(chưa khai báo)'}`));
1183
+ for (const w of gov.warnings) { printWarn(w); totalWarnings++; }
1184
+ for (const o of gov.ok) { printOk(o); totalOk++; }
1185
+ }
1186
+
1187
+ console.log('');
1188
+ }
1189
+
1190
+ // Final summary
1191
+ console.log(chalk.cyan.bold('── Summary ──\n'));
1192
+
1193
+ const total = totalIssues + totalWarnings + totalOk;
1194
+ const score = total > 0 ? Math.round((totalOk / total) * 100) : 0;
1195
+
1196
+ if (totalIssues > 0) {
1197
+ console.log(chalk.red(` ❌ ${totalIssues} issue(s) found`));
1198
+ }
1199
+ if (totalWarnings > 0) {
1200
+ console.log(chalk.yellow(` ⚠️ ${totalWarnings} warning(s)`));
1201
+ }
1202
+ if (legacyWarnings > 0) {
1203
+ console.log(chalk.dim(` 📦 ${legacyWarnings} cảnh báo schema-cũ (tham khảo — migrate + bump schema_version để bật)`));
1204
+ }
1205
+ if (suppressedTotal > 0) {
1206
+ console.log(chalk.dim(` 🔇 ${suppressedTotal} finding(s) ẩn theo .v-flow/validate.config.json`));
1207
+ }
1208
+ console.log(chalk.green(` ✅ ${totalOk} check(s) passed`));
1209
+ console.log(chalk.dim(` 📊 Quality score: ${score}%\n`));
1210
+
1211
+ // Traceability cross-checks (REQ/AC/task coverage, dangling refs) are warnings by
1212
+ // design — they may be transient mid-authoring. Surface --strict so CI / the
1213
+ // pre-implement gate actually blocks on them (closes the "soft gate" gap).
1214
+ if (!options.strict && totalWarnings > 0) {
1215
+ console.log(chalk.dim(` 💡 ${totalWarnings} cảnh báo KHÔNG chặn ở chế độ thường. CI / cổng trước-implement nên chạy: v-flow validate ${featureName || '<feature>'} --strict\n`));
1216
+ }
1217
+
1218
+ if (options.strict && (totalIssues > 0 || totalWarnings > 0)) {
1219
+ process.exit(1);
1220
+ } else if (totalIssues > 0) {
1221
+ process.exit(1);
1222
+ }
1223
+ }));
1224
+ };
1225
+
1226
+ function printArtifactResults(name, result, quiet) {
1227
+ const statusIcon = result.issues.length > 0 ? '❌' : result.warnings.length > 0 ? '⚠️ ' : '✅';
1228
+ console.log(chalk.white(` ${statusIcon} ${name}:`));
1229
+
1230
+ if (!quiet) {
1231
+ for (const item of result.ok) {
1232
+ console.log(chalk.green(` ✅ ${item}`));
1233
+ }
1234
+ }
1235
+ for (const item of result.warnings) {
1236
+ console.log(chalk.yellow(` ⚠️ ${item}`));
1237
+ }
1238
+ for (const item of result.issues) {
1239
+ console.log(chalk.red(` ❌ ${item}`));
1240
+ }
1241
+ }
1242
+
1243
+ // Expose internal validators for unit testing (does not affect command registration)
1244
+ module.exports.validateSpec = validateSpec;
1245
+ module.exports.validatePlan = validatePlan;
1246
+ module.exports.validateTasks = validateTasks;
1247
+ module.exports.validateTestCases = validateTestCases;
1248
+ module.exports.crossValidateReqCoverage = crossValidateReqCoverage;
1249
+ module.exports.crossValidateTaskCoverage = crossValidateTaskCoverage;
1250
+ module.exports.crossValidateNfr = crossValidateNfr;
1251
+ module.exports.crossValidateAcCoverage = crossValidateAcCoverage;
1252
+ module.exports.crossValidateTestAcCoverage = crossValidateTestAcCoverage;
1253
+ module.exports.crossValidatePlanReqCoverage = crossValidatePlanReqCoverage;
1254
+ module.exports.crossValidateDanglingRefs = crossValidateDanglingRefs;
1255
+ module.exports.crossValidatePlanTasks = crossValidatePlanTasks;
1256
+ module.exports.schemaIsLegacy = schemaIsLegacy;
1257
+ module.exports.SCHEMA_VERSION = SCHEMA_VERSION;
1258
+ module.exports.isLiteProfile = isLiteProfile;
1259
+ module.exports.applyIgnore = applyIgnore;