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,442 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readdir, readFile } 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 SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
9
+ const APP_ROUTE_PATTERN = /^(?:src\/)?app\/api\/(.+)\/route\.(js|jsx|ts|tsx)$/;
10
+ const PAGES_ROUTE_PATTERN = /^(?:src\/)?pages\/api\/(.+)\.(js|jsx|ts|tsx)$/;
11
+ const API_STRING_PATTERN = /(['"`])([^'"`]*\/api\/[^'"`]*)\1/g;
12
+ const DIRECT_CALL_PATTERN = /\b([A-Za-z_$][\w$]*)\s*\(/g;
13
+ const KEYWORDS = new Set([
14
+ 'if',
15
+ 'for',
16
+ 'while',
17
+ 'switch',
18
+ 'catch',
19
+ 'function',
20
+ 'return',
21
+ 'await',
22
+ 'fetch',
23
+ 'axios',
24
+ 'console',
25
+ 'setTimeout',
26
+ 'setInterval'
27
+ ]);
28
+
29
+ export async function scanNetworkContracts(repoRoot, options = {}) {
30
+ const root = path.resolve(repoRoot);
31
+ const files = await listRepoFiles(root);
32
+ const routes = collectApiRoutes(files);
33
+ const changedFiles = normalizeChangedFiles(options.changedFiles);
34
+ const scanFiles = changedFiles.length > 0
35
+ ? changedFiles.filter((file) => isSourceFile(file.path)).map((file) => file.path)
36
+ : files.filter(isSourceFile);
37
+ const apiClientCalls = [];
38
+ const serverActionReplacements = [];
39
+
40
+ for (const file of unique(scanFiles)) {
41
+ const currentContent = await readContentForRef(root, file, options.headRef) ?? await readTextIfExists(path.join(root, file));
42
+ if (!currentContent) continue;
43
+ const oldContent = options.baseRef ? await readContentForRef(root, file, options.baseRef) : null;
44
+ const currentCalls = extractApiClientCalls(currentContent, file);
45
+ const oldCalls = oldContent ? extractApiClientCalls(oldContent, file) : [];
46
+ const oldCallKeys = new Set(oldCalls.map(callKey));
47
+ const introducedCalls = currentCalls.map((call) => ({
48
+ ...call,
49
+ introduced_in_diff: changedFiles.length > 0 && !oldCallKeys.has(callKey(call))
50
+ }));
51
+ apiClientCalls.push(...introducedCalls);
52
+
53
+ if (oldContent && introducedCalls.some((call) => call.introduced_in_diff)) {
54
+ const removedServerCalls = detectRemovedServerFunctionCalls(oldContent, currentContent);
55
+ if (removedServerCalls.length > 0) {
56
+ serverActionReplacements.push({
57
+ file,
58
+ removed_calls: removedServerCalls,
59
+ introduced_api_calls: introducedCalls.filter((call) => call.introduced_in_diff),
60
+ risk: 'server_function_replaced_by_http_api'
61
+ });
62
+ }
63
+ }
64
+ }
65
+
66
+ const analyzedCalls = [];
67
+ for (const call of apiClientCalls) {
68
+ const match = matchApiRoute(call.api_path, routes);
69
+ analyzedCalls.push({
70
+ ...call,
71
+ route_status: match.status,
72
+ route_file: match.route?.file ?? null,
73
+ route_path_pattern: match.route?.route_path ?? null,
74
+ cause_candidates: call.introduced_in_diff ? await findCauseCandidates(root, call) : []
75
+ });
76
+ }
77
+
78
+ const missingRoutes = analyzedCalls
79
+ .filter((call) => call.route_status === 'missing')
80
+ .map(toRouteFindingItem);
81
+ const dynamicCalls = analyzedCalls
82
+ .filter((call) => call.route_status === 'dynamic_unresolved')
83
+ .map(toRouteFindingItem);
84
+ const highRiskReplacements = serverActionReplacements
85
+ .filter((item) => item.introduced_api_calls.some((call) => {
86
+ const key = callKey(call);
87
+ return analyzedCalls.some((analyzed) => callKey(analyzed) === key && analyzed.route_status !== 'present');
88
+ }));
89
+
90
+ return {
91
+ schema_version: '0.1.0',
92
+ status: missingRoutes.length > 0 ? 'block' : highRiskReplacements.length > 0 || dynamicCalls.length > 0 ? 'needs_review' : 'pass',
93
+ route_count: routes.length,
94
+ api_client_call_count: analyzedCalls.length,
95
+ introduced_api_client_call_count: analyzedCalls.filter((call) => call.introduced_in_diff).length,
96
+ routes,
97
+ api_client_calls: analyzedCalls,
98
+ missing_routes: missingRoutes,
99
+ dynamic_calls: dynamicCalls,
100
+ server_action_replacements: serverActionReplacements,
101
+ high_risk_replacements: highRiskReplacements,
102
+ risk_summary: {
103
+ missing_routes: summarizeGateEffects(missingRoutes),
104
+ dynamic_calls: summarizeGateEffects(dynamicCalls),
105
+ server_action_replacements: summarizeGateEffects(highRiskReplacements)
106
+ }
107
+ };
108
+ }
109
+
110
+ function collectApiRoutes(files) {
111
+ return files
112
+ .map((file) => {
113
+ const appMatch = APP_ROUTE_PATTERN.exec(file);
114
+ if (appMatch) {
115
+ return {
116
+ router: 'app',
117
+ file,
118
+ route_path: `/api/${appMatch[1]}`,
119
+ matcher: routeMatcherFromSegments(appMatch[1].split('/'))
120
+ };
121
+ }
122
+ const pagesMatch = PAGES_ROUTE_PATTERN.exec(file);
123
+ if (pagesMatch && !pagesMatch[1].startsWith('_')) {
124
+ return {
125
+ router: 'pages',
126
+ file,
127
+ route_path: `/api/${pagesMatch[1].replace(/\/index$/, '')}`,
128
+ matcher: routeMatcherFromSegments(pagesMatch[1].replace(/\/index$/, '').split('/'))
129
+ };
130
+ }
131
+ return null;
132
+ })
133
+ .filter(Boolean);
134
+ }
135
+
136
+ function matchApiRoute(apiPath, routes) {
137
+ if (!apiPath) return { status: 'dynamic_unresolved', route: null };
138
+ const normalized = normalizeApiPath(apiPath.value);
139
+ const match = routes.find((route) => route.matcher.test(normalized));
140
+ if (match) return { status: 'present', route: match };
141
+ if (apiPath.dynamic) {
142
+ const dynamicMatch = routes.find((route) => routeMatchesApiPathPattern(normalized, route.route_path));
143
+ return dynamicMatch
144
+ ? { status: 'present', route: dynamicMatch }
145
+ : { status: 'dynamic_unresolved', route: null };
146
+ }
147
+ return { status: 'missing', route: null };
148
+ }
149
+
150
+ function routeMatcherFromSegments(segments) {
151
+ const pattern = segments
152
+ .filter(Boolean)
153
+ .map((segment) => {
154
+ if (/^\[\[\.\.\.[^\]]+\]\]$/.test(segment)) return '(?:/.*)?';
155
+ if (/^\[\.\.\.[^\]]+\]$/.test(segment)) return '/.+';
156
+ if (/^\[[^\]]+\]$/.test(segment)) return '/[^/]+';
157
+ return `/${escapeRegExp(segment)}`;
158
+ })
159
+ .join('');
160
+ return new RegExp(`^/api${pattern}/?$`);
161
+ }
162
+
163
+ function extractApiClientCalls(content, file) {
164
+ const calls = [];
165
+ const lineStarts = buildLineStarts(content);
166
+ const stripped = stripComments(content);
167
+ for (const match of stripped.matchAll(API_STRING_PATTERN)) {
168
+ const before = stripped.slice(Math.max(0, match.index - 90), match.index);
169
+ const detector = detectClientCall(before);
170
+ if (!detector) continue;
171
+ const rawPath = match[2];
172
+ if (isExternalAbsoluteUrl(rawPath)) continue;
173
+ const apiPath = parseApiPath(rawPath);
174
+ calls.push({
175
+ file,
176
+ line: lineNumberForIndex(lineStarts, match.index),
177
+ callee: detector.callee,
178
+ method: detector.method ?? inferHttpMethod(stripped, match.index, detector),
179
+ api_path: apiPath,
180
+ raw_argument: rawPath,
181
+ static_analysis: apiPath.dynamic ? 'dynamic_path_warning' : 'static_path'
182
+ });
183
+ }
184
+ return calls;
185
+ }
186
+
187
+ function detectClientCall(before) {
188
+ if (/\bfetch\s*\(\s*$/.test(before)) return { callee: 'fetch' };
189
+ const methodCall = /\b([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\.\s*(get|post|put|patch|delete|request)\s*\(\s*$/i.exec(before);
190
+ if (methodCall) return { callee: `${methodCall[1]}.${methodCall[2]}`, method: methodCall[2].toUpperCase() };
191
+ const directCall = /\b(fetchJson|requestJson|apiFetch|apiRequest|get|post|put|patch|delete)\s*\(\s*$/i.exec(before);
192
+ if (directCall) {
193
+ const method = ['get', 'post', 'put', 'patch', 'delete'].includes(directCall[1].toLowerCase())
194
+ ? directCall[1].toUpperCase()
195
+ : null;
196
+ return { callee: directCall[1], method };
197
+ }
198
+ const axiosDirect = /\baxios\s*\(\s*$/.exec(before);
199
+ if (axiosDirect) return { callee: 'axios' };
200
+ return null;
201
+ }
202
+
203
+ function inferHttpMethod(content, matchIndex, detector) {
204
+ if (detector.method) return detector.method;
205
+ const nearby = content.slice(matchIndex, Math.min(content.length, matchIndex + 260));
206
+ const methodMatch = /method\s*:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/i.exec(nearby);
207
+ return methodMatch ? methodMatch[1].toUpperCase() : null;
208
+ }
209
+
210
+ function parseApiPath(rawPath) {
211
+ const apiIndex = rawPath.indexOf('/api/');
212
+ const value = apiIndex >= 0 ? rawPath.slice(apiIndex) : rawPath;
213
+ const normalized = normalizeApiPath(value);
214
+ return {
215
+ value: normalized,
216
+ dynamic: hasDynamicPathSegment(normalized)
217
+ };
218
+ }
219
+
220
+ function isExternalAbsoluteUrl(rawPath) {
221
+ return /^https?:\/\//i.test(String(rawPath ?? ''));
222
+ }
223
+
224
+ function normalizeApiPath(value) {
225
+ const withoutOrigin = String(value ?? '').replace(/^https?:\/\/[^/]+/, '');
226
+ const withoutQuery = withoutOrigin.split(/[?#]/)[0];
227
+ return withoutQuery.replace(/\/+$/, '') || '/api';
228
+ }
229
+
230
+ function hasDynamicPathSegment(apiPath) {
231
+ return splitApiSegments(apiPath).some((segment) => (
232
+ segment.includes('${')
233
+ || segment.includes('*')
234
+ || /^\[[^\]]+\]$/.test(segment)
235
+ ));
236
+ }
237
+
238
+ function routeMatchesApiPathPattern(apiPath, routePath) {
239
+ const callSegments = splitApiSegments(apiPath);
240
+ const routeSegments = splitApiSegments(routePath);
241
+ return matchSegments(callSegments, routeSegments);
242
+ }
243
+
244
+ function matchSegments(callSegments, routeSegments) {
245
+ let callIndex = 0;
246
+ let routeIndex = 0;
247
+ while (routeIndex < routeSegments.length) {
248
+ const routeSegment = routeSegments[routeIndex];
249
+ const callSegment = callSegments[callIndex];
250
+ if (isOptionalCatchAllSegment(routeSegment)) {
251
+ return true;
252
+ }
253
+ if (callSegment === undefined) return false;
254
+ if (isCatchAllSegment(routeSegment)) {
255
+ return callIndex < callSegments.length;
256
+ }
257
+ if (!segmentsMatch(callSegment, routeSegment)) return false;
258
+ callIndex += 1;
259
+ routeIndex += 1;
260
+ }
261
+ return callIndex === callSegments.length;
262
+ }
263
+
264
+ function segmentsMatch(callSegment, routeSegment) {
265
+ if (isDynamicCallSegment(callSegment)) return isDynamicRouteSegment(routeSegment);
266
+ if (isDynamicRouteSegment(routeSegment)) return true;
267
+ return callSegment === routeSegment;
268
+ }
269
+
270
+ function splitApiSegments(apiPath) {
271
+ return normalizeApiPath(apiPath)
272
+ .replace(/^\/api\/?/, '')
273
+ .split('/')
274
+ .filter(Boolean);
275
+ }
276
+
277
+ function isDynamicCallSegment(segment) {
278
+ return /^\$\{[^}]+\}$/.test(segment)
279
+ || segment === '*'
280
+ || /^\[[^\]]+\]$/.test(segment);
281
+ }
282
+
283
+ function isDynamicRouteSegment(segment) {
284
+ return /^\[[^\]]+\]$/.test(segment);
285
+ }
286
+
287
+ function isCatchAllSegment(segment) {
288
+ return /^\[\.\.\.[^\]]+\]$/.test(segment);
289
+ }
290
+
291
+ function isOptionalCatchAllSegment(segment) {
292
+ return /^\[\[\.\.\.[^\]]+\]\]$/.test(segment);
293
+ }
294
+
295
+ function detectRemovedServerFunctionCalls(oldContent, currentContent) {
296
+ const oldCalls = extractDirectCalls(oldContent);
297
+ const currentCalls = new Set(extractDirectCalls(currentContent));
298
+ return oldCalls
299
+ .filter((name) => !currentCalls.has(name))
300
+ .filter((name) => /Action$|Detail|Search|Create|Update|Delete|Load|Fetch|Query|Mutation|submit|save|load|search/.test(name))
301
+ .slice(0, 10);
302
+ }
303
+
304
+ function extractDirectCalls(content) {
305
+ const names = [];
306
+ const stripped = stripComments(content);
307
+ for (const match of stripped.matchAll(DIRECT_CALL_PATTERN)) {
308
+ const name = match[1];
309
+ if (KEYWORDS.has(name)) continue;
310
+ const before = stripped.slice(Math.max(0, match.index - 2), match.index);
311
+ if (before.endsWith('.') || before.endsWith('function ')) continue;
312
+ names.push(name);
313
+ }
314
+ return unique(names);
315
+ }
316
+
317
+ function toRouteFindingItem(call) {
318
+ return {
319
+ file: call.file,
320
+ line: call.line,
321
+ api_path: call.api_path.value,
322
+ method: call.method,
323
+ callee: call.callee,
324
+ route_status: call.route_status,
325
+ introduced_in_diff: call.introduced_in_diff,
326
+ cause_candidates: call.cause_candidates,
327
+ gate_effect: call.route_status === 'missing' ? 'block' : 'review'
328
+ };
329
+ }
330
+
331
+ async function findCauseCandidates(root, call) {
332
+ const candidates = [];
333
+ const needles = [call.api_path.value, call.raw_argument].filter(Boolean);
334
+ for (const needle of unique(needles)) {
335
+ const output = await gitOptional(root, ['log', '--oneline', '-S', needle, '--', call.file]);
336
+ if (!output) continue;
337
+ candidates.push(...output.split('\n').filter(Boolean).slice(0, 3).map((line) => ({
338
+ query: needle,
339
+ commit: line
340
+ })));
341
+ }
342
+ return candidates.slice(0, 5);
343
+ }
344
+
345
+ async function readContentForRef(root, file, ref) {
346
+ if (!ref || ref === 'WORKTREE') return null;
347
+ return gitOptional(root, ['show', `${ref}:${file}`]);
348
+ }
349
+
350
+ async function listRepoFiles(root) {
351
+ const output = await gitOptional(root, ['ls-files']);
352
+ const filesystemFiles = await listFilesRecursive(root);
353
+ if (output) return unique([...output.split('\n').filter(Boolean).map(normalizePath), ...filesystemFiles]);
354
+ return filesystemFiles;
355
+ }
356
+
357
+ async function listFilesRecursive(root, dir = root) {
358
+ const entries = await readdir(dir, { withFileTypes: true });
359
+ const files = [];
360
+ for (const entry of entries) {
361
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.vibepro') continue;
362
+ const fullPath = path.join(dir, entry.name);
363
+ if (entry.isDirectory()) files.push(...await listFilesRecursive(root, fullPath));
364
+ else files.push(normalizePath(path.relative(root, fullPath)));
365
+ }
366
+ return files;
367
+ }
368
+
369
+ async function readTextIfExists(filePath) {
370
+ try {
371
+ return await readFile(filePath, 'utf8');
372
+ } catch (error) {
373
+ if (error.code === 'ENOENT') return null;
374
+ throw error;
375
+ }
376
+ }
377
+
378
+ async function gitOptional(cwd, args) {
379
+ try {
380
+ const { stdout } = await execFileAsync('git', args, { cwd, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
381
+ return stdout.trimEnd();
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+
387
+ function normalizeChangedFiles(files) {
388
+ return (files ?? [])
389
+ .map((file) => typeof file === 'string' ? { path: normalizePath(file), status: 'M' } : { ...file, path: normalizePath(file.path) })
390
+ .filter((file) => file.path);
391
+ }
392
+
393
+ function isSourceFile(file) {
394
+ const normalized = normalizePath(file);
395
+ if (!SOURCE_EXTENSIONS.has(path.extname(normalized))) return false;
396
+ return /^(src|app|pages|components|features|lib|server)\//.test(normalized);
397
+ }
398
+
399
+ function callKey(call) {
400
+ return `${call.file}:${call.api_path.value}:${call.callee}:${call.method ?? ''}`;
401
+ }
402
+
403
+ function summarizeGateEffects(hits) {
404
+ const summary = { block: 0, review: 0, info: 0 };
405
+ for (const hit of hits) {
406
+ const effect = ['block', 'review', 'info'].includes(hit.gate_effect) ? hit.gate_effect : 'info';
407
+ summary[effect] += 1;
408
+ }
409
+ return summary;
410
+ }
411
+
412
+ function buildLineStarts(content) {
413
+ const starts = [0];
414
+ for (let index = 0; index < content.length; index += 1) {
415
+ if (content[index] === '\n') starts.push(index + 1);
416
+ }
417
+ return starts;
418
+ }
419
+
420
+ function lineNumberForIndex(lineStarts, index) {
421
+ let line = 0;
422
+ while (line + 1 < lineStarts.length && lineStarts[line + 1] <= index) line += 1;
423
+ return line + 1;
424
+ }
425
+
426
+ function stripComments(content) {
427
+ return String(content)
428
+ .replace(/\/\*[\s\S]*?\*\//g, '')
429
+ .replace(/(^|[^:])\/\/.*$/gm, '$1');
430
+ }
431
+
432
+ function normalizePath(filePath) {
433
+ return String(filePath ?? '').replace(/\\/g, '/').replace(/^\.\//, '');
434
+ }
435
+
436
+ function escapeRegExp(value) {
437
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
438
+ }
439
+
440
+ function unique(items) {
441
+ return [...new Set(items)];
442
+ }