@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,379 @@
1
+ /**
2
+ * Knowledge Oracle — the structural-knowledge CONTRACT for module-card scoring.
3
+ *
4
+ * Decision 4 (docs/module-card-scoring-decisions.md): the scorer depends on an
5
+ * INTERFACE, not on GitNexus directly — so the provable tier never hard-locks a
6
+ * vendor (keeps H3 portability). Implementations degrade HONESTLY: every method
7
+ * returns `{ supported: boolean, ... }` so a missing capability is reported as
8
+ * "unverified", never silently treated as a pass.
9
+ *
10
+ * Tiers:
11
+ * - NullOracle → nothing supported → scorecard mode "declared-only" (†)
12
+ * - LightweightOracle → symbolExists (file+text) + changedSince (git);
13
+ * coverage/impact need a full graph → unsupported (*)
14
+ * - (future) full-graph adapter (GitNexus) → all supported
15
+ *
16
+ * Contract methods:
17
+ * symbolExists({file, line, name}) → { supported, exists, reason? }
18
+ * changedSince(file, sinceISO) → { supported, changed, commits?, reason? }
19
+ * symbolsOf(module) → { supported, symbols?, reason? } (coverage)
20
+ * impact(symbol) → { supported, ... , reason? } (Phase 3)
21
+ */
22
+
23
+ const path = require('path');
24
+ const fs = require('fs-extra');
25
+ const { execFileSync } = require('child_process');
26
+
27
+ // Extensions of files we can hand straight to `node` (bypassing any shim).
28
+ const NODE_SCRIPT_EXT = new Set(['.js', '.cjs', '.mjs']);
29
+
30
+ function escapeRegExp(s) {
31
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+
34
+ // ════════════════════════════════════════════════════════════
35
+ // NullOracle — declared-only mode
36
+ // ════════════════════════════════════════════════════════════
37
+
38
+ class NullOracle {
39
+ get name() {
40
+ return 'declared-only';
41
+ }
42
+ symbolExists() {
43
+ return { supported: false, reason: 'no oracle' };
44
+ }
45
+ changedSince() {
46
+ return { supported: false, reason: 'no oracle' };
47
+ }
48
+ symbolsOf() {
49
+ return { supported: false, reason: 'no oracle' };
50
+ }
51
+ impact() {
52
+ return { supported: false, reason: 'no oracle' };
53
+ }
54
+ }
55
+
56
+ // ════════════════════════════════════════════════════════════
57
+ // LightweightOracle — symbol index (file+text) + git drift, no graph
58
+ // ════════════════════════════════════════════════════════════
59
+
60
+ class LightweightOracle {
61
+ constructor(projectRoot) {
62
+ this.root = projectRoot;
63
+ this._cache = new Map();
64
+ }
65
+ get name() {
66
+ return 'lightweight';
67
+ }
68
+
69
+ _read(absPath) {
70
+ if (!this._cache.has(absPath)) {
71
+ this._cache.set(absPath, fs.readFileSync(absPath, 'utf8'));
72
+ }
73
+ return this._cache.get(absPath);
74
+ }
75
+
76
+ /** Does `file` exist and (if given) contain symbol `name`? Repo-relative paths. */
77
+ symbolExists({ file, line, name }) {
78
+ if (!file) return { supported: true, exists: false, reason: 'empty ref' };
79
+ const abs = path.join(this.root, file);
80
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
81
+ return { supported: true, exists: false, reason: 'file not found' };
82
+ }
83
+ if (!name) return { supported: true, exists: true };
84
+ const content = this._read(abs);
85
+ const re = new RegExp(`\\b${escapeRegExp(name)}\\b`);
86
+ if (re.test(content)) return { supported: true, exists: true };
87
+ return { supported: true, exists: false, reason: `symbol "${name}" not in file` };
88
+ }
89
+
90
+ /** Has `file` changed since `sinceISO` (YYYY-MM-DD)? Pure git, no graph. */
91
+ changedSince(file, sinceISO) {
92
+ if (!sinceISO) return { supported: true, changed: null, reason: 'no last_verified date' };
93
+ const abs = path.join(this.root, file);
94
+ if (!fs.existsSync(abs)) return { supported: true, changed: null, reason: 'file not found' };
95
+ try {
96
+ const out = execFileSync(
97
+ 'git',
98
+ ['log', `--since=${sinceISO}`, '--oneline', '--', file],
99
+ { cwd: this.root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
100
+ ).trim();
101
+ return { supported: true, changed: out.length > 0, commits: out ? out.split('\n').length : 0 };
102
+ } catch {
103
+ return { supported: false, reason: 'git unavailable' };
104
+ }
105
+ }
106
+
107
+ /** Coverage needs full symbol enumeration → out of scope for the lightweight tier. */
108
+ symbolsOf() {
109
+ return { supported: false, reason: 'coverage cần full-graph oracle (GitNexus)' };
110
+ }
111
+ impact() {
112
+ return { supported: false, reason: 'impact cần full-graph oracle (GitNexus)' };
113
+ }
114
+ }
115
+
116
+ // ════════════════════════════════════════════════════════════
117
+ // GitNexusOracle — full-graph adapter (shells out to `npx gitnexus`)
118
+ // ════════════════════════════════════════════════════════════
119
+
120
+ /** Parse the first column of a gitnexus `cypher` markdown table → values[]. */
121
+ function parseCypherColumn(markdown) {
122
+ if (!markdown) return [];
123
+ const rows = markdown.split('\n').filter((l) => l.trim().startsWith('|'));
124
+ // rows[0] = header, rows[1] = "| --- |" separator, rest = data
125
+ return rows
126
+ .slice(2)
127
+ .map((l) => l.split('|').map((s) => s.trim()).filter((s) => s.length > 0)[0])
128
+ .filter(Boolean);
129
+ }
130
+
131
+ /**
132
+ * Map an execFileSync failure to a human reason instead of a silent null.
133
+ * Pure (error → string) so it's unit-testable without spawning a process.
134
+ */
135
+ function classifyRunnerError(err) {
136
+ if (err.code === 'ENOENT') return 'không tìm thấy `gitnexus` lẫn `npx` trên PATH — cài GitNexus, hoặc đặt env VFLOW_GITNEXUS_BIN=/đường-dẫn/tới/gitnexus';
137
+ if (err.code === 'ETIMEDOUT' || err.signal === 'SIGTERM') return 'gitnexus timeout (>120s) — index quá lớn hoặc treo';
138
+ const stderr = (err.stderr || '').toString().trim();
139
+ if (/no .*index|not indexed|chưa .*index|run .*analyze|gitnexus analyze/i.test(stderr)) {
140
+ return 'repo chưa được index — chạy `npx gitnexus analyze`';
141
+ }
142
+ if (/multiple repos|nhiều repo|specify .*repo|--repo/i.test(stderr)) {
143
+ return 'nhiều repo được index — truyền `--repo <tên>`';
144
+ }
145
+ if (stderr) return `gitnexus lỗi: ${stderr.split('\n')[0].slice(0, 160)}`;
146
+ return `gitnexus thoát mã ${err.status !== null && err.status !== undefined ? err.status : '?'} (không có stderr)`;
147
+ }
148
+
149
+ /**
150
+ * Resolve how to invoke gitnexus, in priority order:
151
+ * 1. env VFLOW_GITNEXUS_BIN — explicit path/command the user points at (a `.js`
152
+ * entry is run via `node`; anything else is exec'd directly).
153
+ * 2. `node <gitnexus-entry.js>` — the JS bin resolved from the install. This is
154
+ * shell-free and the ONLY thing that works on Windows, where `gitnexus`/`npx`
155
+ * are `.cmd` shims that execFileSync can't launch without shell:true — and
156
+ * shell:true is unsafe here, the cypher query contains `|`.
157
+ * 3. `gitnexus` directly on PATH — POSIX node-shebang shim (PATH is augmented so
158
+ * its shebang finds node); the common path when nothing is on Windows.
159
+ * 4. `npx gitnexus` — last resort (resolvable npm pkg only).
160
+ * Returns [command, leadingArgs][] to try until one is found (ENOENT → next).
161
+ */
162
+ function gitnexusInvocations() {
163
+ const override = process.env.VFLOW_GITNEXUS_BIN;
164
+ if (override) {
165
+ return NODE_SCRIPT_EXT.has(path.extname(override).toLowerCase())
166
+ ? [[process.execPath, [override]]]
167
+ : [[override, []]];
168
+ }
169
+ const invocations = [];
170
+ const jsEntry = resolveGitnexusJsEntry();
171
+ if (jsEntry) invocations.push([process.execPath, [jsEntry]]);
172
+ invocations.push(['gitnexus', []], ['npx', ['gitnexus']]);
173
+ return invocations;
174
+ }
175
+
176
+ /** A file we can run as `node <file>`: JS extension, or a `#!…node` shebang. */
177
+ function isNodeScript(file) {
178
+ try {
179
+ if (NODE_SCRIPT_EXT.has(path.extname(file).toLowerCase())) return true;
180
+ const firstLine = fs.readFileSync(file, 'utf8').split('\n', 1)[0] || '';
181
+ return firstLine.startsWith('#!') && /node/.test(firstLine);
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ /** Read a package's `bin` for gitnexus and resolve it to an absolute entry file. */
188
+ function binEntryFromPackage(pkgDir) {
189
+ try {
190
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
191
+ const bin = pkg.bin;
192
+ const rel = typeof bin === 'string' ? bin : bin && (bin.gitnexus || Object.values(bin)[0]);
193
+ if (!rel) return null;
194
+ const entry = path.join(pkgDir, rel);
195
+ return fs.existsSync(entry) ? entry : null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Find gitnexus's JS entry so it can run as `node <entry>` — shell-free, cross-platform,
203
+ * and free of any `.cmd`/shebang dependency. Two strategies, first hit wins:
204
+ * 1. realpath the `gitnexus` shim in a known dir → its node-script target. On POSIX
205
+ * the homebrew/npm symlink points straight at dist/cli/index.js (exact version).
206
+ * 2. read `bin.gitnexus` from the installed package — covers Windows (%APPDATA%\npm)
207
+ * and layouts where the symlink trick doesn't apply.
208
+ * Returns an absolute entry path, or null when gitnexus can't be located.
209
+ */
210
+ function resolveGitnexusJsEntry() {
211
+ const inherited = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
212
+ const searchDirs = [...new Set([...gitnexusBinDirs(), ...inherited])];
213
+
214
+ for (const dir of searchDirs) {
215
+ const shim = path.join(dir, 'gitnexus');
216
+ try {
217
+ if (fs.existsSync(shim)) {
218
+ const real = fs.realpathSync(shim);
219
+ if (isNodeScript(real)) return real;
220
+ }
221
+ } catch {
222
+ /* unreadable dir/symlink → next */
223
+ }
224
+ }
225
+ for (const dir of searchDirs) {
226
+ const pkgDirs = [
227
+ path.join(dir, 'node_modules', 'gitnexus'), // local / %APPDATA%\npm global
228
+ path.join(dir, '..', 'lib', 'node_modules', 'gitnexus'), // posix / homebrew global
229
+ ];
230
+ for (const pkgDir of pkgDirs) {
231
+ const entry = binEntryFromPackage(pkgDir);
232
+ if (entry) return entry;
233
+ }
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /**
239
+ * Directories where gitnexus / npx / node commonly live. Needed because v-flow may
240
+ * run with a STRIPPED PATH — a GUI/Finder launch, a Windows service, or a
241
+ * non-interactive shell that never sourced the user's rc file — where the package
242
+ * manager's bin dir is absent and the bare command can't be found. dirname(
243
+ * process.execPath) is the dir of the node actually running us (so its sibling npx
244
+ * resolves); the rest are the standard global-package bins per platform.
245
+ * - win32: %APPDATA%\npm — where `npm i -g gitnexus` drops gitnexus.cmd (NOT next
246
+ * to node.exe), plus %ProgramFiles%\nodejs.
247
+ * - posix: /opt/homebrew/bin + /usr/local/bin (homebrew / manual installs); these
248
+ * also matter for the `#!/usr/bin/env node` shebang to resolve node.
249
+ */
250
+ function gitnexusBinDirs() {
251
+ const dirs = [path.dirname(process.execPath)];
252
+ if (process.platform === 'win32') {
253
+ if (process.env.APPDATA) dirs.push(path.join(process.env.APPDATA, 'npm'));
254
+ if (process.env.ProgramFiles) dirs.push(path.join(process.env.ProgramFiles, 'nodejs'));
255
+ } else {
256
+ dirs.push('/opt/homebrew/bin', '/usr/local/bin');
257
+ }
258
+ return [...new Set(dirs.filter(Boolean))];
259
+ }
260
+
261
+ /**
262
+ * PATH for the gitnexus subprocess: prepend the dirs above so (a) a bare `gitnexus`/
263
+ * `npx` resolves and (b) their node shebang finds node — even when our own PATH was
264
+ * stripped. The inherited PATH is appended (deduped) so a custom install still works.
265
+ */
266
+ function gitnexusSubprocessPath() {
267
+ const inherited = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
268
+ return [...gitnexusBinDirs(), ...inherited].filter((d, i, a) => a.indexOf(d) === i).join(path.delimiter);
269
+ }
270
+
271
+ /**
272
+ * Default subprocess runner — invokes gitnexus (see gitnexusInvocations).
273
+ * Success → parsed JSON. Failure → `{ __error: <reason> }` (NOT a silent null) so
274
+ * callers can report WHY (missing bin / missing index / non-JSON output) instead
275
+ * of a vague "cypher error". An injected runner may still return plain JSON | null.
276
+ */
277
+ function defaultGitNexusRunner(args, cwd, repo) {
278
+ const full = [...args, ...(repo ? ['--repo', repo] : [])];
279
+ // Hand the child a PATH that includes node + the package-manager bins, so a
280
+ // stripped-PATH launch (GUI/non-interactive shell) can still find gitnexus AND
281
+ // resolve its `#!/usr/bin/env node` shebang.
282
+ const env = { ...process.env, PATH: gitnexusSubprocessPath() };
283
+ const opts = { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 120000 };
284
+
285
+ let out;
286
+ let lastErr;
287
+ for (const [cmd, lead] of gitnexusInvocations()) {
288
+ try {
289
+ out = execFileSync(cmd, [...lead, ...full], opts);
290
+ lastErr = null;
291
+ break;
292
+ } catch (err) {
293
+ lastErr = err;
294
+ if (err.code === 'ENOENT') continue; // this invocation isn't installed → try next
295
+ break; // gitnexus ran but errored → real error, stop
296
+ }
297
+ }
298
+ if (lastErr) return { __error: classifyRunnerError(lastErr) };
299
+
300
+ try {
301
+ return JSON.parse(out);
302
+ } catch {
303
+ const first = (out || '').trim().split('\n')[0] || '(không có output)';
304
+ return { __error: `gitnexus chạy nhưng output không phải JSON ("${first.slice(0, 120)}") — kiểm tra version/flag CLI` };
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Inherits symbolExists (file+text) + changedSince (git) from LightweightOracle,
310
+ * adds the two graph-only capabilities. `runner` is injectable for testing.
311
+ */
312
+ class GitNexusOracle extends LightweightOracle {
313
+ constructor(projectRoot, { repo = null, runner = null } = {}) {
314
+ super(projectRoot);
315
+ this.repo = repo;
316
+ this._runner = runner || ((args) => defaultGitNexusRunner(args, projectRoot, repo));
317
+ }
318
+ get name() {
319
+ return 'full-graph';
320
+ }
321
+
322
+ /** Blast radius of a symbol via `gitnexus impact`. */
323
+ impact(symbol, direction = 'upstream') {
324
+ if (!symbol) return { supported: true, found: false, reason: 'empty symbol' };
325
+ const j = this._runner(['impact', symbol, '--direction', direction]);
326
+ if (!j || j.__error) return { supported: true, found: false, reason: j && j.__error ? j.__error : 'symbol not found / gitnexus error' };
327
+ const affected = [];
328
+ for (const bucket of Object.values(j.byDepth || {})) {
329
+ for (const it of bucket) affected.push(it.name);
330
+ }
331
+ return {
332
+ supported: true,
333
+ found: true,
334
+ impactedCount: j.impactedCount || 0,
335
+ risk: j.risk || 'UNKNOWN',
336
+ affected,
337
+ };
338
+ }
339
+
340
+ /** Enumerate code symbols within the module's directory scope via `gitnexus cypher`. */
341
+ symbolsOf(_module, scope = {}) {
342
+ const dirs = (scope.dirs || []).filter(Boolean);
343
+ if (dirs.length === 0) return { supported: true, symbols: [], reason: 'no scope (card has no code refs)' };
344
+ // GitNexus cypher supports label disjunction in the MATCH pattern, but not
345
+ // `n:Function OR …` / `n.type IN […]` in WHERE — keep the label in MATCH.
346
+ const where = dirs.map((d) => `n.filePath CONTAINS '${d.replace(/'/g, '')}'`).join(' OR ');
347
+ const q = `MATCH (n:Function|Method|Class) WHERE ${where} RETURN n.name`;
348
+ const j = this._runner(['cypher', q]);
349
+ if (!j || j.__error) return { supported: false, reason: j && j.__error ? j.__error : 'gitnexus cypher không trả kết quả' };
350
+ return { supported: true, symbols: parseCypherColumn(j.markdown) };
351
+ }
352
+ }
353
+
354
+ /**
355
+ * @param {'none'|'lightweight'|'gitnexus'} mode
356
+ * @param {string} projectRoot
357
+ * @param {{repo?: string, runner?: function}} [opts]
358
+ */
359
+ function getOracle(mode, projectRoot, opts = {}) {
360
+ if (mode === 'none') return new NullOracle();
361
+ if (mode === 'gitnexus') return new GitNexusOracle(projectRoot, opts);
362
+ return new LightweightOracle(projectRoot);
363
+ }
364
+
365
+ module.exports = {
366
+ NullOracle,
367
+ LightweightOracle,
368
+ GitNexusOracle,
369
+ getOracle,
370
+ escapeRegExp,
371
+ parseCypherColumn,
372
+ classifyRunnerError,
373
+ gitnexusBinDirs,
374
+ gitnexusSubprocessPath,
375
+ gitnexusInvocations,
376
+ resolveGitnexusJsEntry,
377
+ isNodeScript,
378
+ binEntryFromPackage,
379
+ };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Logger — Structured logging for V-Flow CLI
3
+ *
4
+ * Provides verbosity levels, structured output, and JSON mode.
5
+ * Replaces raw console.log with contextual logging.
6
+ *
7
+ * Verbosity levels:
8
+ * 0 = silent (errors only)
9
+ * 1 = normal (default)
10
+ * 2 = verbose (--verbose flag)
11
+ * 3 = debug (VFLOW_DEBUG=1)
12
+ */
13
+
14
+ const chalk = require('chalk');
15
+
16
+ const LEVELS = {
17
+ SILENT: 0,
18
+ NORMAL: 1,
19
+ VERBOSE: 2,
20
+ DEBUG: 3,
21
+ };
22
+
23
+ let _verbosity = LEVELS.NORMAL;
24
+ let _jsonMode = false;
25
+ let _jsonBuffer = [];
26
+ let _startTime = null;
27
+
28
+ /**
29
+ * Configure logger from CLI options
30
+ * @param {object} options
31
+ * @param {boolean} [options.verbose] - Enable verbose output
32
+ * @param {boolean} [options.json] - Enable JSON output mode
33
+ * @param {boolean} [options.silent] - Suppress all output except errors
34
+ */
35
+ function configure(options = {}) {
36
+ if (options.silent) {
37
+ _verbosity = LEVELS.SILENT;
38
+ } else if (process.env.VFLOW_DEBUG === '1') {
39
+ _verbosity = LEVELS.DEBUG;
40
+ } else if (options.verbose || process.env.VFLOW_VERBOSE === '1') {
41
+ _verbosity = LEVELS.VERBOSE;
42
+ } else {
43
+ _verbosity = LEVELS.NORMAL;
44
+ }
45
+
46
+ _jsonMode = !!options.json;
47
+ _jsonBuffer = [];
48
+ _startTime = Date.now();
49
+ }
50
+
51
+ /**
52
+ * Log at normal level
53
+ */
54
+ function info(message, ...args) {
55
+ if (_jsonMode) return;
56
+ if (_verbosity >= LEVELS.NORMAL) {
57
+ console.log(message, ...args);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Log at verbose level
63
+ */
64
+ function verbose(message, ...args) {
65
+ if (_jsonMode) return;
66
+ if (_verbosity >= LEVELS.VERBOSE) {
67
+ console.log(chalk.dim(message), ...args);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Log at debug level
73
+ */
74
+ function debug(message, ...args) {
75
+ if (_jsonMode) return;
76
+ if (_verbosity >= LEVELS.DEBUG) {
77
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Log an error (always shown)
83
+ */
84
+ function error(message, ...args) {
85
+ if (_jsonMode) {
86
+ _jsonBuffer.push({ level: 'error', message, timestamp: Date.now() });
87
+ return;
88
+ }
89
+ console.error(chalk.red(message), ...args);
90
+ }
91
+
92
+ /**
93
+ * Log a warning (normal+ level)
94
+ */
95
+ function warn(message, ...args) {
96
+ if (_jsonMode) {
97
+ _jsonBuffer.push({ level: 'warn', message, timestamp: Date.now() });
98
+ return;
99
+ }
100
+ if (_verbosity >= LEVELS.NORMAL) {
101
+ console.log(chalk.yellow(message), ...args);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Log a success message
107
+ */
108
+ function success(message, ...args) {
109
+ if (_jsonMode) return;
110
+ if (_verbosity >= LEVELS.NORMAL) {
111
+ console.log(chalk.green(message), ...args);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Log a section header
117
+ */
118
+ function header(message) {
119
+ if (_jsonMode) return;
120
+ if (_verbosity >= LEVELS.NORMAL) {
121
+ console.log(chalk.white.bold(message));
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Log a dimmed info line
127
+ */
128
+ function dim(message, ...args) {
129
+ if (_jsonMode) return;
130
+ if (_verbosity >= LEVELS.NORMAL) {
131
+ console.log(chalk.dim(message), ...args);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Buffer a JSON result entry.
137
+ * In JSON mode, output is buffered and flushed at the end.
138
+ */
139
+ function json(data) {
140
+ if (_jsonMode) {
141
+ _jsonBuffer.push(data);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Flush JSON buffer to stdout
147
+ * @param {object} [metadata] - Additional metadata to include
148
+ */
149
+ function flushJson(metadata = {}) {
150
+ if (!_jsonMode) return;
151
+
152
+ const output = {
153
+ ...metadata,
154
+ duration_ms: _startTime ? Date.now() - _startTime : null,
155
+ results: _jsonBuffer.length === 1 ? _jsonBuffer[0] : _jsonBuffer,
156
+ };
157
+
158
+ console.log(JSON.stringify(output, null, 2));
159
+ _jsonBuffer = [];
160
+ }
161
+
162
+ /**
163
+ * Get elapsed time since configure() was called
164
+ * @returns {string} formatted duration
165
+ */
166
+ function elapsed() {
167
+ if (!_startTime) return '0ms';
168
+ const ms = Date.now() - _startTime;
169
+ if (ms < 1000) return `${ms}ms`;
170
+ return `${(ms / 1000).toFixed(1)}s`;
171
+ }
172
+
173
+ /**
174
+ * Check if in JSON mode
175
+ */
176
+ function isJsonMode() {
177
+ return _jsonMode;
178
+ }
179
+
180
+ /**
181
+ * Get current verbosity level
182
+ */
183
+ function getVerbosity() {
184
+ return _verbosity;
185
+ }
186
+
187
+ module.exports = {
188
+ LEVELS,
189
+ configure,
190
+ info,
191
+ verbose,
192
+ debug,
193
+ error,
194
+ warn,
195
+ success,
196
+ header,
197
+ dim,
198
+ json,
199
+ flushJson,
200
+ elapsed,
201
+ isJsonMode,
202
+ getVerbosity,
203
+ };