@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,304 @@
1
+ /**
2
+ * Module Card Lint — Structural validation for Understand-tier knowledge artifacts
3
+ *
4
+ * Validates module cards in `.v-flow/modules/domain-xxx/`:
5
+ * - File completeness (required card files present)
6
+ * - SKILL.md required headings (HEADING MAP)
7
+ * - Leftover template placeholders / unscanned markers
8
+ * - business-quiz.md: questions phrased in BUSINESS language, not code
9
+ *
10
+ * SCOPE — same charter as cli/commands/validate.js: this is STRUCTURAL lint only.
11
+ * It does NOT judge whether a question is "a good business question" (that is the
12
+ * BA/human answer-key's job, per business-quiz.md design). It only flags
13
+ * deterministic signals that a quiz question leaked code vocabulary — the concrete
14
+ * "BA đọc không hiểu vì dính code" pain. Default severity = warning; the validate
15
+ * command's --strict flag escalates. Markdown-first + regex is intentional.
16
+ *
17
+ * All functions are pure (content/path in → result out) so they unit-test cleanly.
18
+ */
19
+
20
+ const path = require('path');
21
+ const fs = require('fs-extra');
22
+
23
+ // ════════════════════════════════════════════════════════════
24
+ // CARD STRUCTURE CONTRACT
25
+ // ════════════════════════════════════════════════════════════
26
+
27
+ /** Files every generated module card must contain. */
28
+ const REQUIRED_CARD_FILES = [
29
+ 'SKILL.md',
30
+ 'use-cases.md',
31
+ 'api-specs.md',
32
+ 'tech-context.md',
33
+ 'cross-service.md',
34
+ 'db.md',
35
+ 'state-diagram.md',
36
+ 'permissions.md',
37
+ 'business-quiz.md',
38
+ 'dev-quiz.md',
39
+ ];
40
+
41
+ /** Present only when the module has a frontend — absence is not an error. */
42
+ const OPTIONAL_CARD_FILES = ['ui-flows.md'];
43
+
44
+ /** Headings SKILL.md must contain (from its HEADING MAP). */
45
+ const REQUIRED_SKILL_HEADINGS = [
46
+ '## Quick Summary',
47
+ '## Key Business Rules',
48
+ '## Key Entities',
49
+ '## No-Go Zones', // matched loosely (emoji/suffix tolerated)
50
+ '## 📂 Supporting Files',
51
+ ];
52
+
53
+ /** Headings business-quiz.md must contain. */
54
+ const REQUIRED_QUIZ_HEADINGS = [
55
+ '## MUST',
56
+ '## SHOULD',
57
+ ];
58
+
59
+ // High-signal residue that should never survive in a real, scanned card.
60
+ const UNSCANNED_MARKER = /\[❓[^\]]*\]/; // [❓ CẦN SCAN CODE] → issue
61
+ const LITERAL_DATE_PLACEHOLDER = /\bYYYY-MM-DD\b/; // unfilled date → warning
62
+ // Raw template tokens (curly placeholders that carry template instruction words).
63
+ const TEMPLATE_TOKEN = /\{(Tên|tên|version|date|Câu hỏi|Lý do|N |~X|file_thực|function_thực|EntityName|table_name|field\d|RelatedEntity|mô tả|danh sách|giải thích|đoạn code|rule|Rule)[^}]*\}/;
64
+ const AI_INFERRED_MARKER = /\[⚠️\s*AI-inferred\]/g; // unverified business rule → count
65
+
66
+ // ════════════════════════════════════════════════════════════
67
+ // BUSINESS-QUIZ CODE-TOKEN DETECTION
68
+ // ════════════════════════════════════════════════════════════
69
+
70
+ /**
71
+ * Deterministic signals that a question is phrased in CODE rather than BUSINESS
72
+ * language. Each is a strong signal in Vietnamese prose (which uses neither
73
+ * underscores nor internal capitals nor backticks).
74
+ */
75
+ const CODE_TOKEN_PATTERNS = [
76
+ { name: 'backtick code', re: /`[^`]+`/g },
77
+ { name: 'file/extension', re: /\b[\w/-]+\.(?:js|jsx|ts|tsx|dart|java|py|rb|go|php|cs|sql|ya?ml|json|kt|swift|cpp|hpp)\b/gi },
78
+ { name: 'function call', re: /\b[A-Za-z_]\w*\(\s*\)/g },
79
+ { name: 'camelCase', re: /\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b/g },
80
+ { name: 'snake_case', re: /\b[a-z][a-z0-9]*_[a-z0-9_]+\b/g },
81
+ { name: 'CONSTANT_CASE', re: /\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\b/g },
82
+ { name: 'API endpoint', re: /\/(?:api|v\d)\/[\w/{}-]+/gi },
83
+ ];
84
+
85
+ /** Remove HTML comments so template guidance/examples are never linted. */
86
+ function stripComments(content) {
87
+ return content.replace(/<!--[\s\S]*?-->/g, '');
88
+ }
89
+
90
+ /**
91
+ * Extract quiz questions from business-quiz.md.
92
+ * @returns {Array<{id, name, questionText}>}
93
+ */
94
+ function extractQuizQuestions(content) {
95
+ const clean = stripComments(content);
96
+ const lines = clean.split('\n');
97
+ const questions = [];
98
+
99
+ let current = null;
100
+ let capturing = false; // inside the question body
101
+ let sawLabel = false; // an explicit "**Câu hỏi**" label was seen for `current`
102
+ let buffer = [];
103
+
104
+ const flush = () => {
105
+ if (current) {
106
+ current.questionText = buffer.join('\n').trim();
107
+ questions.push(current);
108
+ }
109
+ buffer = [];
110
+ capturing = false;
111
+ sawLabel = false;
112
+ };
113
+
114
+ // Accept "**Câu hỏi**", "**Câu hỏi:**", "** Câu hỏi **", with/without trailing colon.
115
+ const QUESTION_LABEL = /^\s*\*\*\s*Câu hỏi\s*:?\s*\*\*\s*:?\s*/i;
116
+ // A body ends at the next bold field ("**Lý do…**"), a divider, or a heading.
117
+ const isStop = (l) => /^\s*\*\*/.test(l) || /^---\s*$/.test(l) || /^#{2,3}\s/.test(l);
118
+
119
+ for (const line of lines) {
120
+ const qHeading = line.match(/^###\s+(Q\d+)\s*[:.)-]?\s*(.*)$/i);
121
+ if (qHeading) {
122
+ flush();
123
+ current = { id: qHeading[1].toUpperCase(), name: (qHeading[2] || '').replace(/\[.*?\]/g, '').trim(), questionText: '' };
124
+ continue;
125
+ }
126
+ if (!current) continue;
127
+
128
+ // Explicit "**Câu hỏi**" label → capture the rest of this line + following body.
129
+ if (QUESTION_LABEL.test(line)) {
130
+ capturing = true;
131
+ sawLabel = true;
132
+ buffer.push(line.replace(QUESTION_LABEL, ''));
133
+ continue;
134
+ }
135
+ if (capturing) {
136
+ if (isStop(line)) { capturing = false; continue; }
137
+ buffer.push(line);
138
+ continue;
139
+ }
140
+ // Fallback: no "**Câu hỏi**" label — treat the first prose line(s) directly
141
+ // under "### QNN" as the question body (cards that omit the label still count).
142
+ if (!sawLabel && buffer.length === 0 && line.trim() && !isStop(line)) {
143
+ capturing = true;
144
+ buffer.push(line);
145
+ }
146
+ }
147
+ flush();
148
+
149
+ return questions.filter((q) => q.questionText.length > 0);
150
+ }
151
+
152
+ /**
153
+ * Find unique code tokens in a question's text.
154
+ * @returns {Array<{token, kind}>}
155
+ */
156
+ function findCodeTokens(questionText) {
157
+ const found = new Map(); // token → kind (first match wins)
158
+ for (const { name, re } of CODE_TOKEN_PATTERNS) {
159
+ const matches = questionText.match(re);
160
+ if (!matches) continue;
161
+ for (const m of matches) {
162
+ const token = m.trim();
163
+ if (token && !found.has(token)) found.set(token, name);
164
+ }
165
+ }
166
+ return [...found.entries()].map(([token, kind]) => ({ token, kind }));
167
+ }
168
+
169
+ /**
170
+ * Lint a business-quiz.md for code-coupled questions + structure.
171
+ * @param {string} content
172
+ * @returns {{ issues: string[], warnings: string[], ok: string[] }}
173
+ */
174
+ function lintBusinessQuiz(content) {
175
+ const issues = [];
176
+ const warnings = [];
177
+ const ok = [];
178
+
179
+ // Required headings — tolerant match (emoji/case/suffix ok), same spirit as
180
+ // SKILL.md's hasHeading. The template ships "## MUST — Câu Hỏi Bắt Buộc" and
181
+ // authors add emoji ("## 🔴 MUST …"); a strict substring check false-flagged those.
182
+ const clean = stripComments(content);
183
+ for (const h of REQUIRED_QUIZ_HEADINGS) {
184
+ if (!hasHeading(clean, h)) {
185
+ issues.push(`business-quiz.md: thiếu heading "${h}" (hoặc biến thể MUST/SHOULD)`);
186
+ }
187
+ }
188
+
189
+ // Answer-leak: the file is designed to contain NO answers (human is the key).
190
+ if (/^#{2,3}\s+(Đáp án|Answer|Đáp Án)\b/im.test(clean)) {
191
+ warnings.push('business-quiz.md: phát hiện mục "Đáp án/Answer" — file quiz KHÔNG được chứa đáp án (human BA/PO là answer key)');
192
+ }
193
+
194
+ const questions = extractQuizQuestions(content);
195
+ if (questions.length === 0) {
196
+ warnings.push('business-quiz.md: không tìm thấy câu hỏi nào (### Q01 …)');
197
+ return { issues, warnings, ok };
198
+ }
199
+
200
+ let leaked = 0;
201
+ for (const q of questions) {
202
+ const tokens = findCodeTokens(q.questionText);
203
+ if (tokens.length === 0) continue;
204
+ leaked++;
205
+ const preview = tokens.slice(0, 5).map((t) => `${t.token} (${t.kind})`).join(', ');
206
+ const more = tokens.length > 5 ? ` …+${tokens.length - 5}` : '';
207
+ warnings.push(`${q.id}: câu hỏi dính ngôn ngữ code → BA khó đọc: ${preview}${more}`);
208
+ }
209
+
210
+ if (leaked === 0) {
211
+ ok.push(`${questions.length} câu hỏi đều dùng ngôn ngữ nghiệp vụ (không dính code)`);
212
+ } else {
213
+ ok.push(`${questions.length - leaked}/${questions.length} câu hỏi dùng ngôn ngữ nghiệp vụ`);
214
+ }
215
+
216
+ return { issues, warnings, ok };
217
+ }
218
+
219
+ // ════════════════════════════════════════════════════════════
220
+ // CARD STRUCTURE VALIDATION
221
+ // ════════════════════════════════════════════════════════════
222
+
223
+ /** Loose heading presence check (tolerates emoji / trailing qualifiers). */
224
+ function hasHeading(content, heading) {
225
+ if (content.includes(heading)) return true;
226
+ // Match on the core text after stripping emoji/punct for No-Go style headings.
227
+ const core = heading.replace(/^#+\s*/, '').replace(/[^\p{L}\p{N} ]/gu, '').trim().toLowerCase();
228
+ return content
229
+ .split('\n')
230
+ .some((l) => /^#{2,3}\s/.test(l) && l.replace(/[^\p{L}\p{N} ]/gu, '').toLowerCase().includes(core));
231
+ }
232
+
233
+ /**
234
+ * Validate one module-card directory (files + headings + placeholders).
235
+ * Pure over an injected file map for testability; the CLI passes a real reader.
236
+ *
237
+ * @param {string} cardName — e.g. "domain-orders"
238
+ * @param {(file: string) => (string|null)} readFile — returns content or null if absent
239
+ * @returns {{ issues: string[], warnings: string[], ok: string[] }}
240
+ */
241
+ function validateModuleCardStructure(cardName, readFile) {
242
+ const issues = [];
243
+ const warnings = [];
244
+ const ok = [];
245
+
246
+ // 1. Required files present
247
+ const missing = [];
248
+ const present = [];
249
+ for (const f of REQUIRED_CARD_FILES) {
250
+ if (readFile(f) === null || readFile(f) === undefined) missing.push(f);
251
+ else present.push(f);
252
+ }
253
+ if (missing.length) issues.push(`thiếu file bắt buộc: ${missing.join(', ')}`);
254
+ else ok.push(`đủ ${REQUIRED_CARD_FILES.length} file card bắt buộc`);
255
+
256
+ for (const f of OPTIONAL_CARD_FILES) {
257
+ if (readFile(f) !== null && readFile(f) !== undefined) present.push(f);
258
+ }
259
+
260
+ // 2. SKILL.md headings
261
+ const skill = readFile('SKILL.md');
262
+ if (skill !== null && skill !== undefined) {
263
+ const skillClean = stripComments(skill);
264
+ const missingH = REQUIRED_SKILL_HEADINGS.filter((h) => !hasHeading(skillClean, h));
265
+ if (missingH.length) issues.push(`SKILL.md: thiếu heading: ${missingH.join(' · ')}`);
266
+ else ok.push('SKILL.md đủ heading bắt buộc');
267
+ }
268
+
269
+ // 3. Placeholder / unscanned-marker sweep across present files
270
+ let aiInferred = 0;
271
+ for (const f of present) {
272
+ const content = readFile(f);
273
+ if (content === null || content === undefined) continue;
274
+ const clean = stripComments(content);
275
+ if (UNSCANNED_MARKER.test(clean)) {
276
+ issues.push(`${f}: còn marker chưa scan code (❓ CẦN SCAN) — card chưa đối chiếu code thật`);
277
+ }
278
+ if (TEMPLATE_TOKEN.test(clean)) {
279
+ warnings.push(`${f}: còn placeholder template chưa điền ({…})`);
280
+ }
281
+ if (LITERAL_DATE_PLACEHOLDER.test(clean)) {
282
+ warnings.push(`${f}: còn ngày placeholder "YYYY-MM-DD" chưa điền`);
283
+ }
284
+ const inferred = clean.match(AI_INFERRED_MARKER);
285
+ if (inferred) aiInferred += inferred.length;
286
+ }
287
+ if (aiInferred > 0) {
288
+ warnings.push(`${aiInferred} business rule còn nhãn [⚠️ AI-inferred] — chưa được human xác nhận`);
289
+ }
290
+
291
+ return { issues, warnings, ok };
292
+ }
293
+
294
+ module.exports = {
295
+ REQUIRED_CARD_FILES,
296
+ OPTIONAL_CARD_FILES,
297
+ REQUIRED_SKILL_HEADINGS,
298
+ CODE_TOKEN_PATTERNS,
299
+ stripComments,
300
+ extractQuizQuestions,
301
+ findCodeTokens,
302
+ lintBusinessQuiz,
303
+ validateModuleCardStructure,
304
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Module Card Score — objective tier of the scorecard (Decision 3).
3
+ *
4
+ * Computes the 6-point OBJECTIVE half of a module card's score from facts a
5
+ * machine can re-check. Pure: (cardName, readFile, oracle) → scorecard object.
6
+ * Anyone re-running with the same oracle gets the same number — that IS the proof.
7
+ *
8
+ * structural (2) — files/headings/placeholders + quiz language [no oracle]
9
+ * grounded (2) — BR `Source:` refs resolve to real code [oracle.symbolExists]
10
+ * freshness (1) — referenced files unchanged since last_verified [oracle.changedSince]
11
+ * coverage (1) — % of module symbols referenced [oracle.symbolsOf → full graph]
12
+ *
13
+ * HONEST DEGRADATION: a dimension the oracle can't verify is marked
14
+ * { verified: false, score: 0 } with a reason — never silently counted as a pass.
15
+ * Mode stamp: all verified → none · coverage-only missing → "*" · grounded/
16
+ * freshness also missing (NullOracle) → "†".
17
+ *
18
+ * Subjective half (business/dev correctness, depth) is NOT computed here — it
19
+ * needs human + AI graders (Decision 2/3).
20
+ */
21
+
22
+ const { validateModuleCardStructure, lintBusinessQuiz, REQUIRED_CARD_FILES, OPTIONAL_CARD_FILES } = require('./module-card-lint');
23
+
24
+ const ALL_CARD_FILES = [...REQUIRED_CARD_FILES, ...OPTIONAL_CARD_FILES];
25
+
26
+ function round1(n) {
27
+ return Math.round(n * 10) / 10;
28
+ }
29
+
30
+ /**
31
+ * Extract code references of the form: Source: `path/file.ext:123`, hàm `funcName`
32
+ * @returns {Array<{file, line, name, raw}>}
33
+ */
34
+ function extractRefs(content) {
35
+ const refs = [];
36
+ const re = /Source:\s*`([^`]+)`(?:[^\n]*?hàm\s*`([^`]+)`)?/gi;
37
+ let m;
38
+ while ((m = re.exec(content)) !== null) {
39
+ const fileLine = m[1].trim();
40
+ const name = m[2] ? m[2].trim() : null;
41
+ let file = fileLine;
42
+ let line = null;
43
+ const idx = fileLine.lastIndexOf(':');
44
+ if (idx > 0 && /^\d+$/.test(fileLine.slice(idx + 1))) {
45
+ file = fileLine.slice(0, idx);
46
+ line = parseInt(fileLine.slice(idx + 1), 10);
47
+ }
48
+ refs.push({ file, line, name, raw: m[0].trim() });
49
+ }
50
+ return refs;
51
+ }
52
+
53
+ /** Pull last-verified date from SKILL.md (body "**Last verified**" or frontmatter). */
54
+ function extractLastVerified(skillContent) {
55
+ if (!skillContent) return null;
56
+ const m =
57
+ skillContent.match(/\*\*Last verified\*\*\s*:?\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i) ||
58
+ skillContent.match(/last_updated\s*:\s*["']?([0-9]{4}-[0-9]{2}-[0-9]{2})/i);
59
+ return m ? m[1] : null;
60
+ }
61
+
62
+ /**
63
+ * @param {string} cardName
64
+ * @param {(file:string)=>(string|null)} readFile
65
+ * @param {object} oracle — KnowledgeOracle implementation
66
+ * @returns {object} scorecard
67
+ */
68
+ function scoreModuleCard(cardName, readFile, oracle) {
69
+ const dimensions = [];
70
+
71
+ // ── 1. Structural (2) — no oracle ──────────────────────────
72
+ const struct = validateModuleCardStructure(cardName, readFile);
73
+ const quizContent = readFile('business-quiz.md');
74
+ const quizR = quizContent !== null && quizContent !== undefined ? lintBusinessQuiz(quizContent) : { issues: [], warnings: [], ok: [] };
75
+ const issues = struct.issues.length + quizR.issues.length;
76
+ const warnings = struct.warnings.length + quizR.warnings.length;
77
+ const structScore = Math.max(0, Math.min(2, 2 - issues * 0.5 - warnings * 0.1));
78
+ dimensions.push({
79
+ key: 'structural',
80
+ max: 2,
81
+ score: round1(structScore),
82
+ verified: true,
83
+ evidence: { issues, warnings, formula: '2 - issues*0.5 - warnings*0.1' },
84
+ });
85
+
86
+ // Gather all card content + refs once. Dedupe identical refs — the same code
87
+ // citation appearing in several card files counts once.
88
+ const allContent = ALL_CARD_FILES.map((f) => readFile(f)).filter(Boolean).join('\n');
89
+ const seen = new Set();
90
+ const refs = extractRefs(allContent).filter((r) => {
91
+ const key = `${r.file}:${r.line}:${r.name}`;
92
+ if (seen.has(key)) return false;
93
+ seen.add(key);
94
+ return true;
95
+ });
96
+
97
+ // ── 2. Grounded (2) — oracle.symbolExists ──────────────────
98
+ if (refs.length === 0) {
99
+ dimensions.push({
100
+ key: 'grounded',
101
+ max: 2,
102
+ score: 0,
103
+ verified: false,
104
+ evidence: { refs_total: 0, reason: 'không có ref code (Source:) để đối chiếu' },
105
+ });
106
+ } else {
107
+ const probe = oracle.symbolExists(refs[0]);
108
+ if (!probe.supported) {
109
+ dimensions.push({
110
+ key: 'grounded',
111
+ max: 2,
112
+ score: 0,
113
+ verified: false,
114
+ evidence: { refs_total: refs.length, reason: probe.reason || 'oracle không hỗ trợ' },
115
+ });
116
+ } else {
117
+ const broken = [];
118
+ let resolved = 0;
119
+ for (const ref of refs) {
120
+ const r = oracle.symbolExists(ref);
121
+ if (r.exists) resolved++;
122
+ else broken.push(`${ref.raw} — ${r.reason || 'unresolved'}`);
123
+ }
124
+ dimensions.push({
125
+ key: 'grounded',
126
+ max: 2,
127
+ score: round1((resolved / refs.length) * 2),
128
+ verified: true,
129
+ evidence: { refs_total: refs.length, refs_resolved: resolved, broken: broken.slice(0, 10) },
130
+ });
131
+ }
132
+ }
133
+
134
+ // ── 3. Freshness / no-drift (1) — oracle.changedSince ──────
135
+ const lastVerified = extractLastVerified(readFile('SKILL.md'));
136
+ const refFiles = [...new Set(refs.map((r) => r.file))];
137
+ if (refFiles.length === 0) {
138
+ dimensions.push({
139
+ key: 'freshness',
140
+ max: 1,
141
+ score: 0,
142
+ verified: false,
143
+ evidence: { reason: 'không có file tham chiếu để kiểm drift' },
144
+ });
145
+ } else {
146
+ const probe = oracle.changedSince(refFiles[0], lastVerified);
147
+ if (!probe.supported || !lastVerified) {
148
+ dimensions.push({
149
+ key: 'freshness',
150
+ max: 1,
151
+ score: 0,
152
+ verified: false,
153
+ evidence: { last_verified: lastVerified, reason: !lastVerified ? 'thiếu Last verified trong SKILL.md' : probe.reason },
154
+ });
155
+ } else {
156
+ const drifted = [];
157
+ let unchanged = 0;
158
+ let checkable = 0;
159
+ for (const f of refFiles) {
160
+ const r = oracle.changedSince(f, lastVerified);
161
+ if (r.changed === null) continue; // not checkable (e.g. file missing) — handled by grounded
162
+ checkable++;
163
+ if (r.changed) drifted.push(`${f} (${r.commits} commit sau ${lastVerified})`);
164
+ else unchanged++;
165
+ }
166
+ dimensions.push({
167
+ key: 'freshness',
168
+ max: 1,
169
+ score: checkable === 0 ? 0 : round1((unchanged / checkable) * 1),
170
+ verified: checkable > 0,
171
+ evidence: {
172
+ last_verified: lastVerified,
173
+ files_checked: checkable,
174
+ drifted: drifted.slice(0, 10),
175
+ // checkable===0 → mọi file ref đều không kiểm được (thiếu file). Không có
176
+ // reason ở đây thì evidenceLine in ra "undefined".
177
+ ...(checkable === 0 ? { reason: 'không có file tham chiếu nào tồn tại để kiểm drift (xem chiều Bằng chứng)' } : {}),
178
+ },
179
+ });
180
+ }
181
+ }
182
+
183
+ // ── 4. Coverage (1) — oracle.symbolsOf (full graph) ────────
184
+ // Scope = directories of the card's referenced files (the area it "owns").
185
+ const refDirs = [...new Set(refFiles.map((f) => (f.includes('/') ? f.slice(0, f.lastIndexOf('/')) : '.')))];
186
+ const cov = oracle.symbolsOf(cardName, { files: refFiles, dirs: refDirs });
187
+ if (!cov.supported) {
188
+ dimensions.push({
189
+ key: 'coverage',
190
+ max: 1,
191
+ score: 0,
192
+ verified: false,
193
+ evidence: { reason: cov.reason || 'cần full-graph oracle' },
194
+ });
195
+ } else {
196
+ const total = cov.symbols ? cov.symbols.length : 0;
197
+ const mentioned = total === 0 ? 0 : cov.symbols.filter((s) => allContent.includes(s)).length;
198
+ dimensions.push({
199
+ key: 'coverage',
200
+ max: 1,
201
+ score: total === 0 ? 0 : round1((mentioned / total) * 1),
202
+ verified: total > 0,
203
+ evidence: { symbols_total: total, symbols_mentioned: mentioned },
204
+ });
205
+ }
206
+
207
+ // ── Totals + honest mode stamp ─────────────────────────────
208
+ const total = round1(dimensions.reduce((s, d) => s + d.score, 0));
209
+ const fullyVerified = dimensions.every((d) => d.verified);
210
+ const stamp = fullyVerified ? '' : oracle.name === 'declared-only' ? '†' : '*';
211
+
212
+ return {
213
+ module: cardName,
214
+ oracle: oracle.name,
215
+ objective_total: total,
216
+ objective_max: 6,
217
+ fully_verified: fullyVerified,
218
+ stamp,
219
+ dimensions,
220
+ };
221
+ }
222
+
223
+ module.exports = { scoreModuleCard, extractRefs, extractLastVerified, round1 };