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,315 @@
1
+ import { execFile } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+
7
+ import { getWorkspaceDir, toWorkspaceRelative } from './workspace.js';
8
+
9
+ const execFileAsync = promisify(execFile);
10
+
11
+ const DECISION_TYPES = new Set(['needs_review', 'noise', 'waiver', 'secret_exposure']);
12
+ const DECISION_STATUSES = new Set(['open', 'accepted', 'rejected', 'superseded']);
13
+ const SECRET_ACTIONS = new Set(['redacted', 'rotated', 'revoked', 'false_positive']);
14
+ const SECRET_PATTERNS = [
15
+ /\bAIza[0-9A-Za-z_-]{20,}\b/g,
16
+ /\bsk-[0-9A-Za-z_-]{20,}\b/g,
17
+ /\b(?:ghp|github_pat)_[0-9A-Za-z_]{20,}\b/g,
18
+ /\b[A-Za-z0-9+/]{32,}={0,2}\b/g
19
+ ];
20
+
21
+ export async function recordDecision(repoRoot, options = {}) {
22
+ const storyId = requireValue(options.storyId, 'decision record requires --id <story-id>');
23
+ const type = requireValue(options.type, 'decision record requires --type <needs_review|noise|waiver|secret_exposure>');
24
+ if (!DECISION_TYPES.has(type)) {
25
+ throw new Error(`decision record --type must be one of: ${[...DECISION_TYPES].join(', ')}`);
26
+ }
27
+ if (!options.summary && !options.stdinText) {
28
+ throw new Error('decision record requires --summary <text> or --from-stdin');
29
+ }
30
+ if (type === 'waiver' && !options.reason) {
31
+ throw new Error('decision record --type waiver requires --reason <text>');
32
+ }
33
+ if (type === 'noise' && !options.reason) {
34
+ throw new Error('decision record --type noise requires --reason <text>');
35
+ }
36
+ if (type === 'secret_exposure') {
37
+ if (!SECRET_ACTIONS.has(options.secretAction)) {
38
+ throw new Error(`decision record --type secret_exposure requires --secret-action ${[...SECRET_ACTIONS].join('|')}`);
39
+ }
40
+ if (!options.secretLocation) {
41
+ throw new Error('decision record --type secret_exposure requires --secret-location <ref>');
42
+ }
43
+ }
44
+
45
+ const root = path.resolve(repoRoot);
46
+ await assertInitializedWorkspace(root);
47
+ const decisionDir = getDecisionDir(root, storyId);
48
+ await mkdir(decisionDir, { recursive: true });
49
+ const evidencePath = getDecisionRecordsPath(root, storyId);
50
+ const existing = await readDecisionRecords(root, storyId);
51
+ const gitContext = await collectGitContext(root);
52
+ const rawText = options.stdinText?.trim() || options.summary;
53
+ const summaryRedaction = redactSecrets(rawText);
54
+ const reasonRedaction = redactSecrets(options.reason ?? '');
55
+ const managedWorktreeWarning = normalizeWarning(options.managedWorktreeWarning);
56
+ const decision = {
57
+ schema_version: '0.1.0',
58
+ decision_id: options.decisionId ?? `decision-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`,
59
+ story_id: storyId,
60
+ type,
61
+ status: normalizeStatus(options.status),
62
+ source: normalizeNullable(options.source),
63
+ source_status: normalizeNullable(options.sourceStatus),
64
+ summary: summaryRedaction.text,
65
+ reason: reasonRedaction.text || null,
66
+ reviewer: normalizeNullable(options.reviewer),
67
+ artifact: options.artifact ? normalizeArtifact(root, options.artifact) : null,
68
+ secret_exposure: type === 'secret_exposure' ? {
69
+ location: options.secretLocation,
70
+ action: options.secretAction,
71
+ value_recorded: false,
72
+ redaction_applied: summaryRedaction.redacted || reasonRedaction.redacted,
73
+ note: 'Secret values are intentionally not stored in VibePro artifacts.'
74
+ } : null,
75
+ redaction: {
76
+ applied: summaryRedaction.redacted || reasonRedaction.redacted,
77
+ hit_count: summaryRedaction.hitCount + reasonRedaction.hitCount
78
+ },
79
+ warnings: managedWorktreeWarning ? [managedWorktreeWarning] : [],
80
+ git_context: gitContext,
81
+ recorded_at: new Date().toISOString()
82
+ };
83
+ const next = {
84
+ schema_version: '0.1.0',
85
+ model: 'vibepro-decision-records-v1',
86
+ story_id: storyId,
87
+ updated_at: new Date().toISOString(),
88
+ warnings: mergeWarnings(existing.warnings, decision.warnings),
89
+ decisions: [
90
+ decision,
91
+ ...existing.decisions.filter((item) => item.decision_id !== decision.decision_id)
92
+ ]
93
+ };
94
+ await writeJsonAtomic(evidencePath, next);
95
+ return {
96
+ decision,
97
+ records: next,
98
+ artifact: toWorkspaceRelative(root, evidencePath)
99
+ };
100
+ }
101
+
102
+ export async function getDecisionStatus(repoRoot, options = {}) {
103
+ const storyId = requireValue(options.storyId, 'decision status requires --id <story-id>');
104
+ const root = path.resolve(repoRoot);
105
+ await assertInitializedWorkspace(root);
106
+ const records = await readDecisionRecords(root, storyId);
107
+ return {
108
+ story_id: storyId,
109
+ artifact: toWorkspaceRelative(root, getDecisionRecordsPath(root, storyId)),
110
+ records,
111
+ summary: summarizeDecisionRecords(records)
112
+ };
113
+ }
114
+
115
+ export async function readDecisionRecordsIfExists(repoRoot, storyId) {
116
+ const root = path.resolve(repoRoot);
117
+ try {
118
+ const records = JSON.parse(await readFile(getDecisionRecordsPath(root, storyId), 'utf8'));
119
+ return {
120
+ ...records,
121
+ decisions: Array.isArray(records.decisions) ? records.decisions : [],
122
+ summary: summarizeDecisionRecords(records),
123
+ artifact: toWorkspaceRelative(root, getDecisionRecordsPath(root, storyId))
124
+ };
125
+ } catch (error) {
126
+ if (error.code === 'ENOENT') return null;
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ export function renderDecisionRecordSummary(result) {
132
+ const warnings = result.decision.warnings?.length
133
+ ? result.decision.warnings.map((warning) => `- ${warning.id}: ${warning.reason}`).join('\n')
134
+ : '- none';
135
+ return `# VibePro Decision Record
136
+
137
+ - story: ${result.decision.story_id}
138
+ - decision: ${result.decision.decision_id}
139
+ - type: ${result.decision.type}
140
+ - status: ${result.decision.status}
141
+ - source: ${result.decision.source ?? '-'}
142
+ - artifact: ${result.artifact}
143
+
144
+ ## Warnings
145
+
146
+ ${warnings}
147
+ `;
148
+ }
149
+
150
+ export function renderDecisionStatusSummary(result) {
151
+ const summary = result.summary;
152
+ return `# VibePro Decision Records
153
+
154
+ - story: ${result.story_id}
155
+ - artifact: ${result.artifact}
156
+ - total: ${summary.total}
157
+ - open: ${summary.open}
158
+ - needs_review: ${summary.by_type.needs_review ?? 0}
159
+ - noise: ${summary.by_type.noise ?? 0}
160
+ - waiver: ${summary.by_type.waiver ?? 0}
161
+ - secret_exposure: ${summary.by_type.secret_exposure ?? 0}
162
+ `;
163
+ }
164
+
165
+ export function summarizeDecisionRecords(records) {
166
+ const decisions = Array.isArray(records?.decisions) ? records.decisions : [];
167
+ const byType = {};
168
+ const byStatus = {};
169
+ for (const decision of decisions) {
170
+ byType[decision.type] = (byType[decision.type] ?? 0) + 1;
171
+ byStatus[decision.status] = (byStatus[decision.status] ?? 0) + 1;
172
+ }
173
+ return {
174
+ total: decisions.length,
175
+ open: decisions.filter((decision) => decision.status === 'open').length,
176
+ by_type: byType,
177
+ by_status: byStatus,
178
+ warnings: Array.isArray(records?.warnings) ? records.warnings : [],
179
+ latest: decisions[0] ?? null
180
+ };
181
+ }
182
+
183
+ async function readDecisionRecords(repoRoot, storyId) {
184
+ try {
185
+ const parsed = JSON.parse(await readFile(getDecisionRecordsPath(repoRoot, storyId), 'utf8'));
186
+ return {
187
+ ...parsed,
188
+ decisions: Array.isArray(parsed.decisions) ? parsed.decisions : []
189
+ };
190
+ } catch (error) {
191
+ if (error.code === 'ENOENT') {
192
+ return {
193
+ schema_version: '0.1.0',
194
+ model: 'vibepro-decision-records-v1',
195
+ story_id: storyId,
196
+ warnings: [],
197
+ decisions: []
198
+ };
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ function getDecisionDir(repoRoot, storyId) {
205
+ return path.join(getWorkspaceDir(repoRoot), 'pr', storyId);
206
+ }
207
+
208
+ function getDecisionRecordsPath(repoRoot, storyId) {
209
+ return path.join(getDecisionDir(repoRoot, storyId), 'decision-records.json');
210
+ }
211
+
212
+ async function assertInitializedWorkspace(repoRoot) {
213
+ try {
214
+ await readFile(path.join(getWorkspaceDir(repoRoot), 'vibepro-manifest.json'), 'utf8');
215
+ } catch (error) {
216
+ if (error.code === 'ENOENT') {
217
+ throw new Error('decision record requires an initialized VibePro workspace. Run `vibepro init <repo>` first.');
218
+ }
219
+ throw error;
220
+ }
221
+ }
222
+
223
+ function requireValue(value, message) {
224
+ if (!value) throw new Error(message);
225
+ return value;
226
+ }
227
+
228
+ function normalizeStatus(status) {
229
+ const value = status ?? 'accepted';
230
+ if (!DECISION_STATUSES.has(value)) {
231
+ throw new Error(`decision record --status must be one of: ${[...DECISION_STATUSES].join(', ')}`);
232
+ }
233
+ return value;
234
+ }
235
+
236
+ function normalizeNullable(value) {
237
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
238
+ }
239
+
240
+ function normalizeArtifact(repoRoot, artifact) {
241
+ return toWorkspaceRelative(repoRoot, path.resolve(repoRoot, artifact));
242
+ }
243
+
244
+ function normalizeWarning(warning) {
245
+ return warning && typeof warning === 'object' ? warning : null;
246
+ }
247
+
248
+ function mergeWarnings(existing = [], next = []) {
249
+ const warnings = [];
250
+ const seen = new Set();
251
+ for (const warning of [...next, ...existing]) {
252
+ if (!warning?.id) continue;
253
+ const key = `${warning.id}:${warning.command_name ?? ''}:${warning.reason ?? ''}`;
254
+ if (seen.has(key)) continue;
255
+ seen.add(key);
256
+ warnings.push(warning);
257
+ }
258
+ return warnings;
259
+ }
260
+
261
+ function redactSecrets(value) {
262
+ let text = String(value ?? '');
263
+ let hitCount = 0;
264
+ for (const pattern of SECRET_PATTERNS) {
265
+ text = text.replace(pattern, (match) => {
266
+ hitCount += 1;
267
+ return `[REDACTED:${hashSecret(match)}]`;
268
+ });
269
+ }
270
+ return {
271
+ text,
272
+ redacted: hitCount > 0,
273
+ hitCount
274
+ };
275
+ }
276
+
277
+ function hashSecret(value) {
278
+ return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
279
+ }
280
+
281
+ async function writeJsonAtomic(filePath, value) {
282
+ const dir = path.dirname(filePath);
283
+ const tempPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
284
+ try {
285
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`);
286
+ await rename(tempPath, filePath);
287
+ } catch (error) {
288
+ await rm(tempPath, { force: true });
289
+ throw error;
290
+ }
291
+ }
292
+
293
+ async function collectGitContext(repoRoot) {
294
+ const [headSha, currentBranch, statusOutput] = await Promise.all([
295
+ gitOptional(repoRoot, ['rev-parse', 'HEAD']),
296
+ gitOptional(repoRoot, ['branch', '--show-current']),
297
+ gitOptional(repoRoot, ['status', '--porcelain', '-uall'])
298
+ ]);
299
+ return {
300
+ head_sha: headSha || null,
301
+ current_branch: currentBranch || null,
302
+ dirty: statusOutput.length > 0,
303
+ status_fingerprint: statusOutput.split('\n').map((line) => line.trimEnd()).filter(Boolean).sort().join('\n'),
304
+ recorded_at: new Date().toISOString()
305
+ };
306
+ }
307
+
308
+ async function gitOptional(repoRoot, args) {
309
+ try {
310
+ const { stdout } = await execFileAsync('git', args, { cwd: repoRoot, encoding: 'utf8' });
311
+ return stdout.trim();
312
+ } catch {
313
+ return '';
314
+ }
315
+ }