@boshu2/vibe-check 1.0.0 → 1.0.2

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.
@@ -1,169 +0,0 @@
1
- import { Commit, VibeCheckResult, OverallRating, Rating } from '../types';
2
- import { calculateIterationVelocity, calculateActiveHours } from './velocity';
3
- import { calculateReworkRatio } from './rework';
4
- import { calculateTrustPassRate } from './trust';
5
- import {
6
- detectFixChains,
7
- calculateDebugSpiralDuration,
8
- calculatePatternSummary,
9
- } from './spirals';
10
- import { calculateFlowEfficiency } from './flow';
11
-
12
- export function analyzeCommits(commits: Commit[]): VibeCheckResult {
13
- if (commits.length === 0) {
14
- return emptyResult();
15
- }
16
-
17
- // Sort commits by date
18
- const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
19
- const from = sorted[0].date;
20
- const to = sorted[sorted.length - 1].date;
21
- const activeHours = calculateActiveHours(sorted);
22
-
23
- // Count commit types
24
- const commitCounts = countCommitTypes(sorted);
25
-
26
- // Detect fix chains
27
- const fixChains = detectFixChains(sorted);
28
-
29
- // Calculate all metrics
30
- const iterationVelocity = calculateIterationVelocity(sorted);
31
- const reworkRatio = calculateReworkRatio(sorted);
32
- const trustPassRate = calculateTrustPassRate(sorted);
33
- const debugSpiralDuration = calculateDebugSpiralDuration(fixChains);
34
- const flowEfficiency = calculateFlowEfficiency(activeHours * 60, fixChains);
35
-
36
- // Calculate pattern summary
37
- const patterns = calculatePatternSummary(fixChains);
38
-
39
- // Determine overall rating
40
- const overall = calculateOverallRating([
41
- iterationVelocity.rating,
42
- reworkRatio.rating,
43
- trustPassRate.rating,
44
- debugSpiralDuration.rating,
45
- flowEfficiency.rating,
46
- ]);
47
-
48
- return {
49
- period: {
50
- from,
51
- to,
52
- activeHours: Math.round(activeHours * 10) / 10,
53
- },
54
- commits: commitCounts,
55
- metrics: {
56
- iterationVelocity,
57
- reworkRatio,
58
- trustPassRate,
59
- debugSpiralDuration,
60
- flowEfficiency,
61
- },
62
- fixChains,
63
- patterns,
64
- overall,
65
- };
66
- }
67
-
68
- function countCommitTypes(commits: Commit[]): VibeCheckResult['commits'] {
69
- const counts = {
70
- total: commits.length,
71
- feat: 0,
72
- fix: 0,
73
- docs: 0,
74
- other: 0,
75
- };
76
-
77
- for (const commit of commits) {
78
- switch (commit.type) {
79
- case 'feat':
80
- counts.feat++;
81
- break;
82
- case 'fix':
83
- counts.fix++;
84
- break;
85
- case 'docs':
86
- counts.docs++;
87
- break;
88
- default:
89
- counts.other++;
90
- }
91
- }
92
-
93
- return counts;
94
- }
95
-
96
- function calculateOverallRating(ratings: Rating[]): OverallRating {
97
- const scores: Record<Rating, number> = {
98
- elite: 4,
99
- high: 3,
100
- medium: 2,
101
- low: 1,
102
- };
103
-
104
- const avgScore =
105
- ratings.reduce((sum, r) => sum + scores[r], 0) / ratings.length;
106
-
107
- if (avgScore >= 3.5) return 'ELITE';
108
- if (avgScore >= 2.5) return 'HIGH';
109
- if (avgScore >= 1.5) return 'MEDIUM';
110
- return 'LOW';
111
- }
112
-
113
- function emptyResult(): VibeCheckResult {
114
- return {
115
- period: {
116
- from: new Date(),
117
- to: new Date(),
118
- activeHours: 0,
119
- },
120
- commits: {
121
- total: 0,
122
- feat: 0,
123
- fix: 0,
124
- docs: 0,
125
- other: 0,
126
- },
127
- metrics: {
128
- iterationVelocity: {
129
- value: 0,
130
- unit: 'commits/hour',
131
- rating: 'low',
132
- description: 'No commits found',
133
- },
134
- reworkRatio: {
135
- value: 0,
136
- unit: '%',
137
- rating: 'elite',
138
- description: 'No commits found',
139
- },
140
- trustPassRate: {
141
- value: 100,
142
- unit: '%',
143
- rating: 'elite',
144
- description: 'No commits found',
145
- },
146
- debugSpiralDuration: {
147
- value: 0,
148
- unit: 'min',
149
- rating: 'elite',
150
- description: 'No debug spirals detected',
151
- },
152
- flowEfficiency: {
153
- value: 100,
154
- unit: '%',
155
- rating: 'elite',
156
- description: 'No active time recorded',
157
- },
158
- },
159
- fixChains: [],
160
- patterns: {
161
- categories: {},
162
- total: 0,
163
- tracerAvailable: 0,
164
- },
165
- overall: 'HIGH',
166
- };
167
- }
168
-
169
- export { calculateActiveHours } from './velocity';
@@ -1,45 +0,0 @@
1
- import { Commit, MetricResult, Rating } from '../types';
2
-
3
- export function calculateReworkRatio(commits: Commit[]): MetricResult {
4
- if (commits.length === 0) {
5
- return {
6
- value: 0,
7
- unit: '%',
8
- rating: 'elite',
9
- description: 'No commits found',
10
- };
11
- }
12
-
13
- const fixCommits = commits.filter((c) => c.type === 'fix').length;
14
- const ratio = (fixCommits / commits.length) * 100;
15
- const rating = getRating(ratio);
16
-
17
- return {
18
- value: Math.round(ratio),
19
- unit: '%',
20
- rating,
21
- description: getDescription(rating, fixCommits, commits.length),
22
- };
23
- }
24
-
25
- function getRating(ratio: number): Rating {
26
- if (ratio < 30) return 'elite';
27
- if (ratio < 50) return 'high';
28
- if (ratio < 70) return 'medium';
29
- return 'low';
30
- }
31
-
32
- function getDescription(rating: Rating, fixes: number, total: number): string {
33
- const fixText = `${fixes}/${total} commits are fixes`;
34
-
35
- switch (rating) {
36
- case 'elite':
37
- return `${fixText}. Mostly forward progress`;
38
- case 'high':
39
- return `${fixText}. Normal for complex work`;
40
- case 'medium':
41
- return `${fixText}. Consider validating assumptions before coding`;
42
- case 'low':
43
- return `${fixText}. High rework, stop and reassess approach`;
44
- }
45
- }
@@ -1,173 +0,0 @@
1
- import { differenceInMinutes } from 'date-fns';
2
- import { Commit, FixChain, MetricResult, Rating } from '../types';
3
-
4
- const SPIRAL_THRESHOLD = 3; // 3+ consecutive fixes = spiral
5
-
6
- // Pattern detection regexes
7
- const PATTERNS: Record<string, RegExp> = {
8
- VOLUME_CONFIG: /volume|mount|path|permission|readonly|pvc|storage/i,
9
- SECRETS_AUTH: /secret|auth|oauth|token|credential|password|key/i,
10
- API_MISMATCH: /api|version|field|spec|schema|crd|resource/i,
11
- SSL_TLS: /ssl|tls|cert|fips|handshake|https/i,
12
- IMAGE_REGISTRY: /image|pull|registry|docker|tag/i,
13
- GITOPS_DRIFT: /drift|sync|argocd|reconcil|outof/i,
14
- };
15
-
16
- export function detectFixChains(commits: Commit[]): FixChain[] {
17
- if (commits.length === 0) return [];
18
-
19
- // Sort by date ascending
20
- const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
21
-
22
- const chains: FixChain[] = [];
23
- let currentChain: Commit[] = [];
24
- let currentComponent: string | null = null;
25
-
26
- for (const commit of sorted) {
27
- if (commit.type === 'fix') {
28
- const component = getComponent(commit);
29
-
30
- if (component === currentComponent || currentComponent === null) {
31
- currentChain.push(commit);
32
- currentComponent = component;
33
- } else {
34
- // Different component, save current chain if valid
35
- if (currentChain.length >= SPIRAL_THRESHOLD) {
36
- chains.push(createFixChain(currentChain, currentComponent));
37
- }
38
- currentChain = [commit];
39
- currentComponent = component;
40
- }
41
- } else {
42
- // Non-fix commit breaks the chain
43
- if (currentChain.length >= SPIRAL_THRESHOLD && currentComponent) {
44
- chains.push(createFixChain(currentChain, currentComponent));
45
- }
46
- currentChain = [];
47
- currentComponent = null;
48
- }
49
- }
50
-
51
- // Handle final chain
52
- if (currentChain.length >= SPIRAL_THRESHOLD && currentComponent) {
53
- chains.push(createFixChain(currentChain, currentComponent));
54
- }
55
-
56
- return chains;
57
- }
58
-
59
- function getComponent(commit: Commit): string {
60
- // Use scope if available
61
- if (commit.scope) {
62
- return commit.scope.toLowerCase();
63
- }
64
-
65
- // Extract first meaningful word from message
66
- const words = commit.message
67
- .replace(/^fix\s*:?\s*/i, '')
68
- .split(/\s+/)
69
- .filter((w) => w.length > 2);
70
-
71
- return words[0]?.toLowerCase() || 'unknown';
72
- }
73
-
74
- function createFixChain(commits: Commit[], component: string): FixChain {
75
- const firstCommit = commits[0].date;
76
- const lastCommit = commits[commits.length - 1].date;
77
- const duration = differenceInMinutes(lastCommit, firstCommit);
78
-
79
- // Detect pattern from commit messages
80
- const allMessages = commits.map((c) => c.message).join(' ');
81
- const pattern = detectPattern(allMessages);
82
-
83
- return {
84
- component,
85
- commits: commits.length,
86
- duration,
87
- isSpiral: commits.length >= SPIRAL_THRESHOLD,
88
- pattern,
89
- firstCommit,
90
- lastCommit,
91
- };
92
- }
93
-
94
- function detectPattern(text: string): string | null {
95
- for (const [pattern, regex] of Object.entries(PATTERNS)) {
96
- if (regex.test(text)) {
97
- return pattern;
98
- }
99
- }
100
- return null;
101
- }
102
-
103
- export function calculateDebugSpiralDuration(chains: FixChain[]): MetricResult {
104
- const spirals = chains.filter((c) => c.isSpiral);
105
-
106
- if (spirals.length === 0) {
107
- return {
108
- value: 0,
109
- unit: 'min',
110
- rating: 'elite',
111
- description: 'No debug spirals detected',
112
- };
113
- }
114
-
115
- const totalDuration = spirals.reduce((sum, s) => sum + s.duration, 0);
116
- const avgDuration = totalDuration / spirals.length;
117
- const rating = getRating(avgDuration);
118
-
119
- return {
120
- value: Math.round(avgDuration),
121
- unit: 'min',
122
- rating,
123
- description: getDescription(rating, spirals.length),
124
- };
125
- }
126
-
127
- function getRating(duration: number): Rating {
128
- if (duration < 15) return 'elite';
129
- if (duration < 30) return 'high';
130
- if (duration < 60) return 'medium';
131
- return 'low';
132
- }
133
-
134
- function getDescription(rating: Rating, spiralCount: number): string {
135
- const spiralText = spiralCount === 1 ? '1 spiral' : `${spiralCount} spirals`;
136
-
137
- switch (rating) {
138
- case 'elite':
139
- return `${spiralText} resolved quickly`;
140
- case 'high':
141
- return `${spiralText}, normal debugging time`;
142
- case 'medium':
143
- return `${spiralText}, consider using tracer tests`;
144
- case 'low':
145
- return `${spiralText}, extended debugging. Use tracer tests before implementation`;
146
- }
147
- }
148
-
149
- export function calculatePatternSummary(chains: FixChain[]): {
150
- categories: Record<string, number>;
151
- total: number;
152
- tracerAvailable: number;
153
- } {
154
- const categories: Record<string, number> = {};
155
- let total = 0;
156
- let withTracer = 0;
157
-
158
- for (const chain of chains) {
159
- const pattern = chain.pattern || 'OTHER';
160
- categories[pattern] = (categories[pattern] || 0) + chain.commits;
161
- total += chain.commits;
162
-
163
- if (chain.pattern) {
164
- withTracer += chain.commits;
165
- }
166
- }
167
-
168
- return {
169
- categories,
170
- total,
171
- tracerAvailable: total > 0 ? Math.round((withTracer / total) * 100) : 0,
172
- };
173
- }
@@ -1,80 +0,0 @@
1
- import { differenceInMinutes } from 'date-fns';
2
- import { Commit, MetricResult, Rating } from '../types';
3
-
4
- const FOLLOWUP_WINDOW_MINUTES = 30;
5
-
6
- export function calculateTrustPassRate(commits: Commit[]): MetricResult {
7
- if (commits.length === 0) {
8
- return {
9
- value: 100,
10
- unit: '%',
11
- rating: 'elite',
12
- description: 'No commits found',
13
- };
14
- }
15
-
16
- // Sort by date ascending
17
- const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
18
-
19
- let trustedCommits = 0;
20
-
21
- for (let i = 0; i < sorted.length; i++) {
22
- const commit = sorted[i];
23
- const nextCommit = sorted[i + 1];
24
-
25
- // Check if next commit is a fix to same component within 30 min
26
- const needsFollowup =
27
- nextCommit &&
28
- nextCommit.type === 'fix' &&
29
- sameComponent(commit, nextCommit) &&
30
- differenceInMinutes(nextCommit.date, commit.date) < FOLLOWUP_WINDOW_MINUTES;
31
-
32
- if (!needsFollowup) {
33
- trustedCommits++;
34
- }
35
- }
36
-
37
- const rate = (trustedCommits / commits.length) * 100;
38
- const rating = getRating(rate);
39
-
40
- return {
41
- value: Math.round(rate),
42
- unit: '%',
43
- rating,
44
- description: getDescription(rating),
45
- };
46
- }
47
-
48
- function sameComponent(a: Commit, b: Commit): boolean {
49
- // If both have scopes, compare them
50
- if (a.scope && b.scope) {
51
- return a.scope.toLowerCase() === b.scope.toLowerCase();
52
- }
53
-
54
- // If neither has scope, check if messages reference same area
55
- // This is a simple heuristic - first word after type
56
- const aWords = a.message.split(/\s+/).slice(0, 3);
57
- const bWords = b.message.split(/\s+/).slice(0, 3);
58
-
59
- return aWords.some((word) => bWords.includes(word) && word.length > 3);
60
- }
61
-
62
- function getRating(rate: number): Rating {
63
- if (rate > 95) return 'elite';
64
- if (rate >= 80) return 'high';
65
- if (rate >= 60) return 'medium';
66
- return 'low';
67
- }
68
-
69
- function getDescription(rating: Rating): string {
70
- switch (rating) {
71
- case 'elite':
72
- return 'Code sticks on first try, high AI trust';
73
- case 'high':
74
- return 'Occasional fixes needed, mostly autonomous';
75
- case 'medium':
76
- return 'Regular intervention required';
77
- case 'low':
78
- return 'Heavy oversight needed, run tracer tests before implementation';
79
- }
80
- }
@@ -1,86 +0,0 @@
1
- import { differenceInMinutes } from 'date-fns';
2
- import { Commit, MetricResult, Rating } from '../types';
3
-
4
- const SESSION_GAP_MINUTES = 120; // 2 hours = new session
5
-
6
- export function calculateIterationVelocity(commits: Commit[]): MetricResult {
7
- if (commits.length === 0) {
8
- return {
9
- value: 0,
10
- unit: 'commits/hour',
11
- rating: 'low',
12
- description: 'No commits found',
13
- };
14
- }
15
-
16
- const activeHours = calculateActiveHours(commits);
17
-
18
- if (activeHours === 0) {
19
- return {
20
- value: commits.length,
21
- unit: 'commits/hour',
22
- rating: 'high',
23
- description: 'All commits in rapid succession',
24
- };
25
- }
26
-
27
- const velocity = commits.length / activeHours;
28
- const rating = getRating(velocity);
29
-
30
- return {
31
- value: Math.round(velocity * 10) / 10,
32
- unit: 'commits/hour',
33
- rating,
34
- description: getDescription(rating),
35
- };
36
- }
37
-
38
- export function calculateActiveHours(commits: Commit[]): number {
39
- if (commits.length < 2) {
40
- return 0.1; // Minimum to avoid division by zero
41
- }
42
-
43
- // Sort by date ascending
44
- const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
45
-
46
- let totalMinutes = 0;
47
- let sessionStart = sorted[0].date;
48
-
49
- for (let i = 1; i < sorted.length; i++) {
50
- const gap = differenceInMinutes(sorted[i].date, sorted[i - 1].date);
51
-
52
- if (gap > SESSION_GAP_MINUTES) {
53
- // End current session, start new one
54
- totalMinutes += differenceInMinutes(sorted[i - 1].date, sessionStart);
55
- sessionStart = sorted[i].date;
56
- }
57
- }
58
-
59
- // Add final session
60
- totalMinutes += differenceInMinutes(sorted[sorted.length - 1].date, sessionStart);
61
-
62
- // Minimum of 10 minutes per session to account for work between commits
63
- const minMinutes = Math.max(totalMinutes, commits.length * 10);
64
-
65
- return minMinutes / 60;
66
- }
67
-
68
- function getRating(velocity: number): Rating {
69
- if (velocity > 5) return 'elite';
70
- if (velocity >= 3) return 'high';
71
- if (velocity >= 1) return 'medium';
72
- return 'low';
73
- }
74
-
75
- function getDescription(rating: Rating): string {
76
- switch (rating) {
77
- case 'elite':
78
- return 'Excellent iteration speed, tight feedback loops';
79
- case 'high':
80
- return 'Good iteration speed';
81
- case 'medium':
82
- return 'Normal pace';
83
- case 'low':
84
- return 'Slow iteration, consider smaller commits';
85
- }
86
- }
@@ -1,20 +0,0 @@
1
- import { VibeCheckResult, OutputFormat } from '../types';
2
- import { formatTerminal } from './terminal';
3
- import { formatJson } from './json';
4
- import { formatMarkdown } from './markdown';
5
-
6
- export function formatOutput(result: VibeCheckResult, format: OutputFormat): string {
7
- switch (format) {
8
- case 'json':
9
- return formatJson(result);
10
- case 'markdown':
11
- return formatMarkdown(result);
12
- case 'terminal':
13
- default:
14
- return formatTerminal(result);
15
- }
16
- }
17
-
18
- export { formatTerminal } from './terminal';
19
- export { formatJson } from './json';
20
- export { formatMarkdown } from './markdown';
@@ -1,51 +0,0 @@
1
- import { VibeCheckResult } from '../types';
2
-
3
- export function formatJson(result: VibeCheckResult): string {
4
- // Create a JSON-friendly version with ISO date strings
5
- const output = {
6
- period: {
7
- from: result.period.from.toISOString(),
8
- to: result.period.to.toISOString(),
9
- activeHours: result.period.activeHours,
10
- },
11
- commits: result.commits,
12
- metrics: {
13
- iterationVelocity: {
14
- value: result.metrics.iterationVelocity.value,
15
- unit: result.metrics.iterationVelocity.unit,
16
- rating: result.metrics.iterationVelocity.rating,
17
- },
18
- reworkRatio: {
19
- value: result.metrics.reworkRatio.value,
20
- unit: result.metrics.reworkRatio.unit,
21
- rating: result.metrics.reworkRatio.rating,
22
- },
23
- trustPassRate: {
24
- value: result.metrics.trustPassRate.value,
25
- unit: result.metrics.trustPassRate.unit,
26
- rating: result.metrics.trustPassRate.rating,
27
- },
28
- debugSpiralDuration: {
29
- value: result.metrics.debugSpiralDuration.value,
30
- unit: result.metrics.debugSpiralDuration.unit,
31
- rating: result.metrics.debugSpiralDuration.rating,
32
- },
33
- flowEfficiency: {
34
- value: result.metrics.flowEfficiency.value,
35
- unit: result.metrics.flowEfficiency.unit,
36
- rating: result.metrics.flowEfficiency.rating,
37
- },
38
- },
39
- fixChains: result.fixChains.map((chain) => ({
40
- component: chain.component,
41
- commits: chain.commits,
42
- duration: chain.duration,
43
- isSpiral: chain.isSpiral,
44
- pattern: chain.pattern,
45
- })),
46
- patterns: result.patterns,
47
- overall: result.overall,
48
- };
49
-
50
- return JSON.stringify(output, null, 2);
51
- }