@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.
- package/AGENTS.md +265 -0
- package/CHANGELOG.md +318 -0
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/agents/ba-agent.md +437 -0
- package/agents/ba-critic-agent.md +156 -0
- package/agents/ba-to-ptyc-agent.md +112 -0
- package/agents/bugfix-analyst-agent.md +221 -0
- package/agents/constitute-agent.md +155 -0
- package/agents/help-agent.md +168 -0
- package/agents/implement-agent.md +220 -0
- package/agents/import-ba-docs-agent.md +164 -0
- package/agents/master-check-agent.md +228 -0
- package/agents/metrics-agent.md +180 -0
- package/agents/operations-agent.md +123 -0
- package/agents/plan-agent.md +218 -0
- package/agents/prototype-agent.md +191 -0
- package/agents/retrospective-agent.md +196 -0
- package/agents/review-agent.md +210 -0
- package/agents/sprint-agent.md +191 -0
- package/agents/status-agent.md +186 -0
- package/agents/sync-agent.md +201 -0
- package/agents/test-agent.md +166 -0
- package/agents/understand-agent.md +339 -0
- package/cli/commands/check.js +96 -0
- package/cli/commands/dev-quiz.js +107 -0
- package/cli/commands/doctor.js +348 -0
- package/cli/commands/feature.js +259 -0
- package/cli/commands/hooks.js +163 -0
- package/cli/commands/init.js +189 -0
- package/cli/commands/log.js +199 -0
- package/cli/commands/plugin.js +230 -0
- package/cli/commands/score-card.js +203 -0
- package/cli/commands/status.js +269 -0
- package/cli/commands/sync.js +59 -0
- package/cli/commands/upgrade.js +150 -0
- package/cli/commands/validate.js +1259 -0
- package/cli/commands/watch.js +151 -0
- package/cli/index.js +46 -0
- package/cli/lib/ac-test-gate.js +89 -0
- package/cli/lib/activity-log.js +209 -0
- package/cli/lib/cli-error.js +183 -0
- package/cli/lib/constitution-lint.js +561 -0
- package/cli/lib/dev-quiz-grade.js +127 -0
- package/cli/lib/governance.js +78 -0
- package/cli/lib/hook-targets.js +167 -0
- package/cli/lib/i18n.js +375 -0
- package/cli/lib/knowledge-oracle.js +379 -0
- package/cli/lib/logger.js +203 -0
- package/cli/lib/module-card-lint.js +304 -0
- package/cli/lib/module-card-score.js +223 -0
- package/cli/lib/plugins.js +481 -0
- package/cli/lib/scanner.js +692 -0
- package/cli/lib/sync-core.js +232 -0
- package/cli/lib/sync-targets.js +84 -0
- package/cli/lib/templates.js +268 -0
- package/cli/lib/yaml-parser.js +203 -0
- package/commands/v.ba-critic.md +101 -0
- package/commands/v.ba-to-ptyc.md +71 -0
- package/commands/v.bugfix.md +86 -0
- package/commands/v.check.md +131 -0
- package/commands/v.constitute.md +87 -0
- package/commands/v.constitution.md +84 -0
- package/commands/v.fork.md +127 -0
- package/commands/v.help.md +73 -0
- package/commands/v.hotfix.md +200 -0
- package/commands/v.implement.md +92 -0
- package/commands/v.import-ba-docs.md +222 -0
- package/commands/v.metrics.md +74 -0
- package/commands/v.operations.md +70 -0
- package/commands/v.plan.md +78 -0
- package/commands/v.prototype.md +121 -0
- package/commands/v.quickfix.md +169 -0
- package/commands/v.retrospective.md +80 -0
- package/commands/v.review.md +78 -0
- package/commands/v.rewind.md +127 -0
- package/commands/v.specify.md +118 -0
- package/commands/v.sprint.md +75 -0
- package/commands/v.status.md +62 -0
- package/commands/v.sync.md +81 -0
- package/commands/v.test.md +67 -0
- package/commands/v.understand.md +112 -0
- package/package.json +65 -0
- package/skills/_shared/constitution-reader/SKILL.md +109 -0
- package/skills/_shared/constitution-reader/config.json +52 -0
- package/skills/_shared/constitution-reader/examples/good/b1-phase-output.md +48 -0
- package/skills/_shared/constitution-reader/gotchas.md +46 -0
- package/skills/_shared/context-reader/SKILL.md +111 -0
- package/skills/_shared/context-reader/config.json +54 -0
- package/skills/_shared/context-reader/examples/good/legacy-nodejs-output.md +35 -0
- package/skills/_shared/context-reader/gotchas.md +49 -0
- package/skills/_shared/ears-notation/SKILL.md +63 -0
- package/skills/_shared/ears-notation/config.json +55 -0
- package/skills/_shared/ears-notation/examples/good/plan-test-interpretation.md +29 -0
- package/skills/_shared/ears-notation/gotchas.md +43 -0
- package/skills/check/cross-validator/SKILL.md +206 -0
- package/skills/check/cross-validator/config.json +33 -0
- package/skills/check/cross-validator/examples/good/validation-report-pass-with-concerns.md +105 -0
- package/skills/check/cross-validator/gotchas.md +43 -0
- package/skills/implement/constitution-enforcer/SKILL.md +134 -0
- package/skills/implement/constitution-enforcer/config.json +16 -0
- package/skills/implement/constitution-enforcer/examples/bad/vague-report.md +42 -0
- package/skills/implement/constitution-enforcer/examples/good/compliance-report.md +57 -0
- package/skills/implement/constitution-enforcer/gotchas.md +26 -0
- package/skills/implement/constitution-enforcer/scripts/check-constitution.sh +88 -0
- package/skills/implement/no-go-zone-guard/SKILL.md +173 -0
- package/skills/implement/no-go-zone-guard/config.json +28 -0
- package/skills/implement/no-go-zone-guard/examples/good/adapter-workaround.md +46 -0
- package/skills/implement/no-go-zone-guard/gotchas.md +27 -0
- package/skills/implement/no-go-zone-guard/scripts/check-nogo-zones.sh +148 -0
- package/skills/implement/no-go-zone-guard/scripts/nogo-precommit.sh +96 -0
- package/skills/implement/tdd-driver/SKILL.md +159 -0
- package/skills/implement/tdd-driver/config.json +33 -0
- package/skills/implement/tdd-driver/examples/good/tdd-cycle-product-repo.md +81 -0
- package/skills/implement/tdd-driver/gotchas.md +34 -0
- package/skills/metrics/metrics-collector/SKILL.md +133 -0
- package/skills/metrics/metrics-collector/config.json +16 -0
- package/skills/metrics/metrics-collector/examples/bad/incomplete-report.md +48 -0
- package/skills/metrics/metrics-collector/examples/good/full-metrics-report.md +101 -0
- package/skills/metrics/metrics-collector/gotchas.md +26 -0
- package/skills/operations/incident-runbook/SKILL.md +167 -0
- package/skills/operations/incident-runbook/config.json +21 -0
- package/skills/operations/incident-runbook/examples/bad/vague-incident-report.md +48 -0
- package/skills/operations/incident-runbook/examples/good/p1-hotfix-response.md +119 -0
- package/skills/operations/incident-runbook/gotchas.md +26 -0
- package/skills/plan/architecture-designer/SKILL.md +228 -0
- package/skills/plan/architecture-designer/config.json +32 -0
- package/skills/plan/architecture-designer/examples/bad/vague-plan.md +62 -0
- package/skills/plan/architecture-designer/examples/good/expand-contract-migration.md +56 -0
- package/skills/plan/architecture-designer/examples/good/plan-structure.md +58 -0
- package/skills/plan/architecture-designer/gotchas.md +45 -0
- package/skills/plan/task-breakdown/SKILL.md +208 -0
- package/skills/plan/task-breakdown/config.json +26 -0
- package/skills/plan/task-breakdown/examples/bad/vague-tasks.md +77 -0
- package/skills/plan/task-breakdown/examples/good/spike-clarify-tasks.md +66 -0
- package/skills/plan/task-breakdown/examples/good/tasks-login-feature.md +111 -0
- package/skills/plan/task-breakdown/gotchas.md +39 -0
- package/skills/prototype/LOGIC.md +240 -0
- package/skills/prototype/SKILL.md +185 -0
- package/skills/prototype/UI.md +407 -0
- package/skills/prototype/config.json +104 -0
- package/skills/prototype/examples/bad/prototype-notes.md +68 -0
- package/skills/prototype/examples/good/prototype-notes-ui.md +109 -0
- package/skills/prototype/examples/good/prototype-notes.md +67 -0
- package/skills/prototype/gotchas.md +128 -0
- package/skills/prototype/scripts/check-flow-state.ps1 +112 -0
- package/skills/prototype/scripts/check-flow-state.sh +104 -0
- package/skills/prototype/scripts/check-prototype-cleanup.ps1 +124 -0
- package/skills/prototype/scripts/check-prototype-cleanup.sh +109 -0
- package/skills/prototype/scripts/check-prototype-notes.ps1 +107 -0
- package/skills/prototype/scripts/check-prototype-notes.sh +102 -0
- package/skills/review/adversarial-reviewer/SKILL.md +137 -0
- package/skills/review/adversarial-reviewer/config.json +32 -0
- package/skills/review/adversarial-reviewer/examples/good/review-report-template.md +56 -0
- package/skills/review/adversarial-reviewer/gotchas.md +46 -0
- package/skills/review/adversarial-reviewer/scripts/quick-security-scan.sh +52 -0
- package/skills/specify/ba-bpmn-doc-gen/SKILL.md +108 -0
- package/skills/specify/ba-bpmn-doc-gen/reference/reference-bpmn-generation.md +528 -0
- package/skills/specify/ba-bpmn-doc-gen/reference/reference-drawio-flowchart.md +466 -0
- package/skills/specify/ba-critic/SKILL.md +172 -0
- package/skills/specify/ba-critic/config.json +32 -0
- package/skills/specify/ba-critic/examples/good/critic-report-round1.md +51 -0
- package/skills/specify/ba-critic/gotchas.md +40 -0
- package/skills/specify/ba-critic/scripts/check-spec-quality.sh +72 -0
- package/skills/specify/ba-doc-generator/SKILL.md +102 -0
- package/skills/specify/ba-doc-generator/references/template-clevel.md +84 -0
- package/skills/specify/ba-doc-generator/references/template-compliance.md +83 -0
- package/skills/specify/ba-doc-generator/references/template-dev.md +138 -0
- package/skills/specify/ba-doc-generator/references/template-partner.md +167 -0
- package/skills/specify/ba-doc-generator/references/template-pm.md +92 -0
- package/skills/specify/ba-doc-generator/references/template-review.md +114 -0
- package/skills/specify/ba-doc-generator/references/template-tester.md +108 -0
- package/skills/specify/ba-doc-generator/references/template-user.md +98 -0
- package/skills/specify/bugfix-analyst/SKILL.md +296 -0
- package/skills/specify/bugfix-analyst/config.json +41 -0
- package/skills/specify/bugfix-analyst/examples/bad/common-mistakes.md +71 -0
- package/skills/specify/bugfix-analyst/examples/good/email-validation-bugfix.md +53 -0
- package/skills/specify/bugfix-analyst/gotchas.md +51 -0
- package/skills/specify/ears-writer/SKILL.md +129 -0
- package/skills/specify/ears-writer/config.json +20 -0
- package/skills/specify/ears-writer/examples/bad/common-mistakes.md +17 -0
- package/skills/specify/ears-writer/examples/good/login-requirements.md +41 -0
- package/skills/specify/ears-writer/gotchas.md +43 -0
- package/skills/specify/ears-writer/scripts/check-ears-compliance.sh +51 -0
- package/skills/test/test-case-generator/SKILL.md +161 -0
- package/skills/test/test-case-generator/config.json +33 -0
- package/skills/test/test-case-generator/examples/good/test-cases-login.md +104 -0
- package/skills/test/test-case-generator/gotchas.md +43 -0
- package/skills/understand/ba-docs-scanner/SKILL.md +239 -0
- package/skills/understand/ba-docs-scanner/config.json +47 -0
- package/skills/understand/ba-docs-scanner/examples/good/work-order-br-extract.md +28 -0
- package/skills/understand/ba-docs-scanner/gotchas.md +44 -0
- package/skills/understand/ba-docs-scanner/merge-rules.md +47 -0
- package/skills/understand/codebase-scanner/SKILL.md +260 -0
- package/skills/understand/codebase-scanner/config.json +56 -0
- package/skills/understand/codebase-scanner/examples/good/menu-module-output.md +44 -0
- package/skills/understand/codebase-scanner/gotchas.md +42 -0
- package/skills/understand/codebase-scanner/scripts/scan-project-structure.sh +64 -0
- package/templates/DESIGN.md +456 -0
- package/templates/agent-command-template.yaml +240 -0
- package/templates/agent-config-template.md +170 -0
- package/templates/agent-definition-template.md +145 -0
- package/templates/agent-metrics-template.md +150 -0
- package/templates/api-contract-template.md +72 -0
- package/templates/bugfix-report-template.md +195 -0
- package/templates/bugfix-spec-template.md +134 -0
- package/templates/code-review-report-template.md +119 -0
- package/templates/constitution-template.md +234 -0
- package/templates/context-template.md +94 -0
- package/templates/data-model-template.md +95 -0
- package/templates/decision-log-template.md +92 -0
- package/templates/flow-state-template.yaml +208 -0
- package/templates/github/workflows/v-flow-validate.yml +30 -0
- package/templates/knowledge/adr-template.md +70 -0
- package/templates/knowledge/api-contract-template.md +140 -0
- package/templates/knowledge/domain-glossary.md +29 -0
- package/templates/knowledge/golden-tests-readme.md +115 -0
- package/templates/knowledge/lessons-learned.md +41 -0
- package/templates/knowledge/patterns.md +103 -0
- package/templates/module-card/SKILL.md +85 -0
- package/templates/module-card/api-specs.md +96 -0
- package/templates/module-card/business-quiz.md +119 -0
- package/templates/module-card/cross-service.md +125 -0
- package/templates/module-card/db.md +85 -0
- package/templates/module-card/dev-quiz.md +62 -0
- package/templates/module-card/permissions.md +83 -0
- package/templates/module-card/state-diagram.md +64 -0
- package/templates/module-card/tech-context.md +90 -0
- package/templates/module-card/ui-flows.md +91 -0
- package/templates/module-card/use-cases.md +142 -0
- package/templates/module-template.yaml +161 -0
- package/templates/operations-report-template.md +108 -0
- package/templates/plan-template.md +308 -0
- package/templates/prototype-notes-template.md +116 -0
- package/templates/ptyc/PTYC.template.docx +0 -0
- package/templates/ptyc/ptyc.meta.example.yaml +44 -0
- package/templates/retrospective-report-template.md +136 -0
- package/templates/security-review-template.md +84 -0
- package/templates/session-template.md +167 -0
- package/templates/spec-review-log-template.md +75 -0
- package/templates/spec-template.md +229 -0
- package/templates/sprint-status-template.md +101 -0
- package/templates/tasks-template.md +275 -0
- package/templates/test-cases-template.md +124 -0
- package/templates/ux-checklist-template.md +79 -0
- package/templates/validation-report-template.md +125 -0
- 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
|
+
};
|