vibepro 0.1.0-alpha.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 (89) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.ja.md +448 -0
  4. package/README.md +520 -0
  5. package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
  6. package/bin/vibepro.js +9 -0
  7. package/docs/assets/vibepro-header.png +0 -0
  8. package/package.json +51 -0
  9. package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
  10. package/skills/vibepro-human-review/SKILL.md +73 -0
  11. package/skills/vibepro-story-refactor/SKILL.md +89 -0
  12. package/skills/vibepro-workflow/SKILL.md +139 -0
  13. package/src/agent-harness-map.js +230 -0
  14. package/src/agent-harness-scanner.js +337 -0
  15. package/src/agent-review.js +2180 -0
  16. package/src/api-boundary-scanner.js +452 -0
  17. package/src/architecture-profiler.js +423 -0
  18. package/src/authorization-scoring.js +149 -0
  19. package/src/brainbase-importer.js +534 -0
  20. package/src/change-risk-classifier.js +195 -0
  21. package/src/check-packs.js +605 -0
  22. package/src/checkpoint-manager.js +233 -0
  23. package/src/cli.js +2213 -0
  24. package/src/code-quality-scanner.js +310 -0
  25. package/src/codex-manager.js +143 -0
  26. package/src/component-style-scanner.js +336 -0
  27. package/src/coverage-report.js +99 -0
  28. package/src/database-access-scanner.js +163 -0
  29. package/src/decision-records.js +315 -0
  30. package/src/design-modernize.js +1435 -0
  31. package/src/design-system.js +1732 -0
  32. package/src/diagnostic-engine.js +1945 -0
  33. package/src/diagram-requirement-resolver.js +194 -0
  34. package/src/doctor.js +677 -0
  35. package/src/environment-graph.js +424 -0
  36. package/src/execution-state.js +849 -0
  37. package/src/explore-evidence.js +425 -0
  38. package/src/flow-design-scanner.js +896 -0
  39. package/src/flow-verifier.js +887 -0
  40. package/src/gesture-interaction-scanner.js +330 -0
  41. package/src/graph-context.js +263 -0
  42. package/src/graphify-adapter.js +189 -0
  43. package/src/html-report.js +1035 -0
  44. package/src/journey-map.js +1299 -0
  45. package/src/language.js +48 -0
  46. package/src/lazy-pattern-detector.js +182 -0
  47. package/src/local-dev-scanner.js +135 -0
  48. package/src/managed-worktree-gate.js +187 -0
  49. package/src/managed-worktree.js +766 -0
  50. package/src/merge-manager.js +501 -0
  51. package/src/network-contract-scanner.js +442 -0
  52. package/src/nocodb-story-sync.js +386 -0
  53. package/src/oss-readiness-scanner.js +417 -0
  54. package/src/performance-evidence.js +756 -0
  55. package/src/performance-measurer.js +591 -0
  56. package/src/pr-manager.js +8220 -0
  57. package/src/presets.js +682 -0
  58. package/src/public-discovery-scanner.js +519 -0
  59. package/src/refactoring-delta-reporter.js +367 -0
  60. package/src/refactoring-opportunity-generator.js +797 -0
  61. package/src/regression-risk-scanner.js +146 -0
  62. package/src/repo-status.js +266 -0
  63. package/src/report-fingerprint.js +188 -0
  64. package/src/report-pr-body-prompt-template.md +108 -0
  65. package/src/report-pr-body-schema.json +95 -0
  66. package/src/report-store.js +135 -0
  67. package/src/report-validator.js +192 -0
  68. package/src/requirement-consistency.js +1066 -0
  69. package/src/runtime-info.js +134 -0
  70. package/src/self-dogfood-scanner.js +476 -0
  71. package/src/session-learning.js +164 -0
  72. package/src/skills-manager.js +157 -0
  73. package/src/spec-drift.js +378 -0
  74. package/src/spec-fingerprint.js +445 -0
  75. package/src/spec-prompt-template.md +155 -0
  76. package/src/spec-schema.json +219 -0
  77. package/src/spec-store.js +258 -0
  78. package/src/spec-validator.js +459 -0
  79. package/src/static-site-scanner.js +316 -0
  80. package/src/story-candidate-generator.js +85 -0
  81. package/src/story-catalog-generator.js +2813 -0
  82. package/src/story-html.js +156 -0
  83. package/src/story-manager.js +2144 -0
  84. package/src/story-task-generator.js +522 -0
  85. package/src/task-manager.js +1029 -0
  86. package/src/terminal-link-scanner.js +238 -0
  87. package/src/usage-report.js +417 -0
  88. package/src/verification-evidence.js +284 -0
  89. package/src/workspace.js +126 -0
@@ -0,0 +1,316 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readdir, readFile, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ const IGNORED_DIRS = new Set([
9
+ '.git',
10
+ '.next',
11
+ '.turbo',
12
+ '.vibepro',
13
+ 'coverage',
14
+ 'node_modules',
15
+ 'graphify-out'
16
+ ]);
17
+ const IGNORED_FILES = new Set(['.gitignore', '.vibeproignore']);
18
+ const TEXT_EXTENSIONS = new Set([
19
+ '.css',
20
+ '.env',
21
+ '.htm',
22
+ '.html',
23
+ '.js',
24
+ '.json',
25
+ '.jsx',
26
+ '.mjs',
27
+ '.md',
28
+ '.py',
29
+ '.ts',
30
+ '.tsx',
31
+ '.txt',
32
+ '.yaml',
33
+ '.yml'
34
+ ]);
35
+ const STATIC_EXTENSIONS = new Set([
36
+ '.css',
37
+ '.gif',
38
+ '.htm',
39
+ '.html',
40
+ '.ico',
41
+ '.jpeg',
42
+ '.jpg',
43
+ '.js',
44
+ '.json',
45
+ '.map',
46
+ '.md',
47
+ '.mjs',
48
+ '.png',
49
+ '.svg',
50
+ '.ttf',
51
+ '.txt',
52
+ '.webp',
53
+ '.woff',
54
+ '.woff2'
55
+ ]);
56
+ const SECRET_PATTERNS = [
57
+ {
58
+ kind: 'secret_keyword',
59
+ pattern: /\b(api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|secret[_-]?key)\b\s*[:=]\s*["']?([A-Za-z0-9_\-.]{8,})/i
60
+ },
61
+ {
62
+ kind: 'openai_key_like',
63
+ pattern: /\bsk-[A-Za-z0-9]{20,}\b/
64
+ }
65
+ ];
66
+ const XSS_PATTERNS = [
67
+ { kind: 'inner_html_assignment', pattern: /\.innerHTML\s*=/ },
68
+ { kind: 'eval_call', pattern: /(?<![\w$])eval\s*\(/ },
69
+ { kind: 'new_function', pattern: /\bnew\s+Function\s*\(/ },
70
+ { kind: 'document_write', pattern: /\bdocument\.write\s*\(/ }
71
+ ];
72
+ const EXTERNAL_RESOURCE_PATTERN =
73
+ /<(script|link|iframe)\b[^>]*(?:src|href)=["'](https?:\/\/[^"']+)["'][^>]*>/gi;
74
+ const GATE_EFFECTS = ['block', 'review', 'info'];
75
+
76
+ export async function scanStaticSite(repoRoot) {
77
+ const root = path.resolve(repoRoot);
78
+ const files = await filterGitIgnoredFiles(root, await collectFiles(root));
79
+ const result = {
80
+ has_index_html: files.some((file) => file.relativePath === 'index.html'),
81
+ scanned_files: files.length,
82
+ secret_hits: [],
83
+ xss_risk_hits: [],
84
+ external_resources: [],
85
+ non_static_files: []
86
+ };
87
+
88
+ for (const file of files) {
89
+ const ext = path.extname(file.relativePath).toLowerCase();
90
+ if (!STATIC_EXTENSIONS.has(ext)) {
91
+ result.non_static_files.push({ file: file.relativePath, extension: ext || '(none)' });
92
+ }
93
+
94
+ if (!TEXT_EXTENSIONS.has(ext) && !isEnvFile(file.relativePath)) continue;
95
+
96
+ const content = await readFile(file.absolutePath, 'utf8');
97
+ const lines = content.split(/\r?\n/);
98
+ for (const [index, line] of lines.entries()) {
99
+ collectSecretHits(result.secret_hits, file.relativePath, index + 1, line);
100
+ collectXssHits(result.xss_risk_hits, file.relativePath, index + 1, line);
101
+ collectExternalResources(result.external_resources, file.relativePath, index + 1, line);
102
+ }
103
+ }
104
+
105
+ result.risk_summary = {
106
+ secret_hits: summarizeGateEffects(result.secret_hits),
107
+ xss_risk_hits: summarizeGateEffects(result.xss_risk_hits)
108
+ };
109
+ return result;
110
+ }
111
+
112
+ async function collectFiles(root, current = root) {
113
+ const entries = await readdir(current, { withFileTypes: true });
114
+ const files = [];
115
+
116
+ for (const entry of entries) {
117
+ if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
118
+ if (entry.isFile() && IGNORED_FILES.has(entry.name)) continue;
119
+ const absolutePath = path.join(current, entry.name);
120
+ const relativePath = path.relative(root, absolutePath).split(path.sep).join('/');
121
+
122
+ if (entry.isDirectory()) {
123
+ files.push(...await collectFiles(root, absolutePath));
124
+ continue;
125
+ }
126
+
127
+ if (!entry.isFile()) continue;
128
+ const fileStat = await stat(absolutePath);
129
+ if (fileStat.size > 1024 * 1024) continue;
130
+ files.push({ absolutePath, relativePath });
131
+ }
132
+
133
+ return files;
134
+ }
135
+
136
+ async function filterGitIgnoredFiles(root, files) {
137
+ if (files.length === 0) return files;
138
+
139
+ try {
140
+ const ignored = new Set();
141
+ for (let index = 0; index < files.length; index += 200) {
142
+ const chunk = files.slice(index, index + 200).map((file) => file.relativePath);
143
+ try {
144
+ const { stdout } = await execFileAsync('git', ['check-ignore', ...chunk], {
145
+ cwd: root,
146
+ encoding: 'utf8',
147
+ maxBuffer: 1024 * 1024
148
+ });
149
+ for (const file of stdout.split(/\r?\n/).filter(Boolean)) {
150
+ ignored.add(file);
151
+ }
152
+ } catch (error) {
153
+ if (error.code !== 1) throw error;
154
+ }
155
+ }
156
+ return files.filter((file) => !ignored.has(file.relativePath));
157
+ } catch (error) {
158
+ return files;
159
+ }
160
+ }
161
+
162
+ function collectSecretHits(hits, file, lineNumber, line) {
163
+ if (isEnvFile(file) && line.trim() && !line.trim().startsWith('#')) {
164
+ if (isSafeEnvFileLine(line)) return;
165
+ const risk = classifySecretRisk(file, line, 'env_file_value');
166
+ hits.push({
167
+ file,
168
+ line: lineNumber,
169
+ kind: 'env_file_value',
170
+ excerpt: maskSensitiveLine(line),
171
+ ...risk
172
+ });
173
+ return;
174
+ }
175
+
176
+ for (const { kind, pattern } of SECRET_PATTERNS) {
177
+ if (!pattern.test(line)) continue;
178
+ const risk = classifySecretRisk(file, line, kind);
179
+ hits.push({
180
+ file,
181
+ line: lineNumber,
182
+ kind,
183
+ excerpt: maskSensitiveLine(line),
184
+ ...risk
185
+ });
186
+ pattern.lastIndex = 0;
187
+ return;
188
+ }
189
+ }
190
+
191
+ function isEnvFile(file) {
192
+ const basename = path.basename(file);
193
+ return basename === '.env' || basename.startsWith('.env.');
194
+ }
195
+
196
+ function isSafeEnvFileLine(line) {
197
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line.trim());
198
+ if (!match) return false;
199
+
200
+ const [, key, rawValue] = match;
201
+ if (key.startsWith('DOTENV_PUBLIC_KEY')) return true;
202
+
203
+ return /^"?encrypted:/.test(rawValue.trim());
204
+ }
205
+
206
+ function collectXssHits(hits, file, lineNumber, line) {
207
+ for (const { kind, pattern } of XSS_PATTERNS) {
208
+ if (!pattern.test(line)) continue;
209
+ const risk = classifyXssRisk(file, line, kind);
210
+ hits.push({
211
+ file,
212
+ line: lineNumber,
213
+ kind,
214
+ excerpt: line.trim().slice(0, 160),
215
+ ...risk
216
+ });
217
+ }
218
+ }
219
+
220
+ function collectExternalResources(resources, file, lineNumber, line) {
221
+ EXTERNAL_RESOURCE_PATTERN.lastIndex = 0;
222
+ let match = EXTERNAL_RESOURCE_PATTERN.exec(line);
223
+ while (match) {
224
+ resources.push({
225
+ file,
226
+ line: lineNumber,
227
+ tag: match[1].toLowerCase(),
228
+ url: match[2]
229
+ });
230
+ match = EXTERNAL_RESOURCE_PATTERN.exec(line);
231
+ }
232
+ }
233
+
234
+ function maskSensitiveLine(line) {
235
+ return line
236
+ .trim()
237
+ .replace(/sk-[A-Za-z0-9]{20,}/g, (value) => `${value.slice(0, 6)}...${value.slice(-4)}`)
238
+ .replace(/(["']?)([A-Za-z0-9_\-.]{12,})(["']?)/g, (_match, prefix, value, suffix) => {
239
+ if (!/[A-Za-z]/.test(value) || !/[0-9]/.test(value)) return `${prefix}${value}${suffix}`;
240
+ return `${prefix}${value.slice(0, 4)}...${value.slice(-4)}${suffix}`;
241
+ });
242
+ }
243
+
244
+ function classifySecretRisk(file, line, kind) {
245
+ const sourceKind = classifySourceKind(file);
246
+ if (sourceKind !== 'runtime_code') {
247
+ return { source_kind: sourceKind, confidence: 'low', gate_effect: 'info' };
248
+ }
249
+ if (isPlaceholderSecret(line) || isEnvironmentReference(line) || isSecretReferenceOnly(line, kind)) {
250
+ return { source_kind: sourceKind, confidence: 'low', gate_effect: 'info' };
251
+ }
252
+ if (kind === 'env_file_value' || kind === 'openai_key_like' || /\bsk-[A-Za-z0-9]{20,}\b/.test(line)) {
253
+ return { source_kind: sourceKind, confidence: 'high', gate_effect: 'block' };
254
+ }
255
+ return { source_kind: sourceKind, confidence: 'medium', gate_effect: 'review' };
256
+ }
257
+
258
+ function classifyXssRisk(file, line, kind) {
259
+ const sourceKind = classifySourceKind(file);
260
+ if (sourceKind !== 'runtime_code') {
261
+ return { source_kind: sourceKind, confidence: 'low', gate_effect: 'info' };
262
+ }
263
+ if (kind === 'inner_html_assignment' && /DOMPurify\.sanitize|sanitize\(/.test(line)) {
264
+ return { source_kind: sourceKind, confidence: 'low', gate_effect: 'info' };
265
+ }
266
+ return { source_kind: sourceKind, confidence: 'medium', gate_effect: 'review' };
267
+ }
268
+
269
+ function classifySourceKind(file) {
270
+ const normalized = file.toLowerCase();
271
+ const basename = path.basename(normalized);
272
+ if (normalized.startsWith('.claude/')) return 'agent_skill';
273
+ if (normalized.startsWith('public/ttyd/')) return 'vendor_bundle';
274
+ if (basename === '.env.example' || basename === '.env.sample' || basename === '.env.template') return 'example';
275
+ if (normalized.startsWith('docs/') || normalized.endsWith('.md')) return 'docs';
276
+ if (/(^|\/)(__tests__|tests?|spec|fixtures?)(\/|$)/.test(normalized)
277
+ || /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(normalized)) {
278
+ return 'test';
279
+ }
280
+ if (/(^|\/)(examples?|samples?)(\/|$)/.test(normalized)) return 'example';
281
+ return 'runtime_code';
282
+ }
283
+
284
+ function isPlaceholderSecret(line) {
285
+ return /\b(example|dummy|placeholder|your[_-]?|xxxx|xxxxx|test[_-]?key)\b/i.test(line)
286
+ || /[xX]{8,}/.test(line)
287
+ || /<[^>]*(key|token|secret)[^>]*>/i.test(line);
288
+ }
289
+
290
+ function isEnvironmentReference(line) {
291
+ return /\bprocess\.env\b|\bos\.environ\b|\bos\.getenv\s*\(|\benv\.[A-Z0-9_]+\b/i.test(line);
292
+ }
293
+
294
+ function isSecretReferenceOnly(line, kind) {
295
+ if (kind !== 'secret_keyword') return false;
296
+ const match = /\b(api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|secret[_-]?key)\b\s*[:=]\s*([^,;\n)\]}]+)/i.exec(line);
297
+ if (!match) return false;
298
+ const value = match[2].trim();
299
+ if (!value) return true;
300
+ if (/^['"`]/.test(value)) return false;
301
+ if (/^(request|req|body|params|headers|cookies|formData)\b/i.test(value)) return true;
302
+ if (/^[A-Za-z_$][\w$]*\s*\($/.test(value)) return true;
303
+ if (!/^[A-Za-z_$][\w$]*(?:[.?!][A-Za-z_$][\w$]*)*(?:\s*\(|\s*$|[.?!,\]])/.test(value)) return false;
304
+ return /[.?!]/.test(value)
305
+ || /\b[A-Za-z]+(?:Key|Token|Secret)\b/.test(value)
306
+ || /(?:^|_)(api_key|api_secret|access_token|auth_token|secret_key)(?:_|$)/i.test(value);
307
+ }
308
+
309
+ function summarizeGateEffects(hits) {
310
+ const summary = Object.fromEntries(GATE_EFFECTS.map((effect) => [effect, 0]));
311
+ for (const hit of hits) {
312
+ const effect = GATE_EFFECTS.includes(hit.gate_effect) ? hit.gate_effect : 'info';
313
+ summary[effect] += 1;
314
+ }
315
+ return summary;
316
+ }
@@ -0,0 +1,85 @@
1
+ const MIN_CLUSTER_SIZE = 2;
2
+ const HIGH_CONFIDENCE_THRESHOLD = 8;
3
+ const MEDIUM_CONFIDENCE_THRESHOLD = 4;
4
+ const CLUSTER_DEPTH_LEVELS = [4, 2];
5
+
6
+ export function generateStoryCandidates(coverage) {
7
+ const uncovered = Array.isArray(coverage?.uncovered) ? coverage.uncovered : [];
8
+ if (uncovered.length === 0) return [];
9
+
10
+ const byRole = groupBy(uncovered, (item) => item.role || 'unknown');
11
+ const candidates = [];
12
+ const seenIds = new Set();
13
+
14
+ for (const [role, items] of Object.entries(byRole)) {
15
+ for (const depth of CLUSTER_DEPTH_LEVELS) {
16
+ const clusters = clusterByCommonPath(items, depth);
17
+ for (const cluster of clusters) {
18
+ if (cluster.paths.length < MIN_CLUSTER_SIZE) continue;
19
+ const candidate = buildCandidate(role, cluster);
20
+ if (seenIds.has(candidate.candidate_id)) continue;
21
+ seenIds.add(candidate.candidate_id);
22
+ candidates.push(candidate);
23
+ }
24
+ }
25
+ }
26
+
27
+ return candidates.sort((a, b) => b.file_count - a.file_count || a.candidate_id.localeCompare(b.candidate_id));
28
+ }
29
+
30
+ function clusterByCommonPath(items, depth) {
31
+ const buckets = new Map();
32
+ for (const item of items) {
33
+ const segments = item.path.split('/').filter(Boolean);
34
+ if (segments.length === 0) continue;
35
+ const dirSegments = segments.slice(0, -1);
36
+ const prefix = dirSegments.length === 0
37
+ ? segments[0]
38
+ : dirSegments.slice(0, depth).join('/');
39
+ if (!buckets.has(prefix)) buckets.set(prefix, { common_path: prefix, paths: [] });
40
+ buckets.get(prefix).paths.push(item.path);
41
+ }
42
+ return [...buckets.values()];
43
+ }
44
+
45
+ function buildCandidate(role, cluster) {
46
+ const slug = slugifyPath(cluster.common_path);
47
+ const fileCount = cluster.paths.length;
48
+ const confidence = inferConfidence(fileCount);
49
+ return {
50
+ candidate_id: `candidate-${role}-${slug}`,
51
+ role,
52
+ common_path: cluster.common_path,
53
+ paths: cluster.paths.slice(0, 12),
54
+ file_count: fileCount,
55
+ confidence,
56
+ evidence: cluster.paths.slice(0, 5),
57
+ open_questions: [
58
+ '対応する Story / Spec が既に存在するか確認',
59
+ 'この粒度で新規 Story 化するか、既存 Story へ吸収するか判断'
60
+ ],
61
+ suggested_story_titles: [
62
+ `${cluster.common_path} の責務を Story 化する`
63
+ ]
64
+ };
65
+ }
66
+
67
+ function inferConfidence(fileCount) {
68
+ if (fileCount >= HIGH_CONFIDENCE_THRESHOLD) return 'high';
69
+ if (fileCount >= MEDIUM_CONFIDENCE_THRESHOLD) return 'medium';
70
+ return 'low';
71
+ }
72
+
73
+ function slugifyPath(commonPath) {
74
+ return commonPath.replace(/[^a-zA-Z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase();
75
+ }
76
+
77
+ function groupBy(items, keyFn) {
78
+ const out = {};
79
+ for (const item of items) {
80
+ const key = keyFn(item);
81
+ if (!out[key]) out[key] = [];
82
+ out[key].push(item);
83
+ }
84
+ return out;
85
+ }