@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.
- package/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +89 -65
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/package.json +7 -4
- package/vitest.config.ts +14 -0
- package/src/cli.ts +0 -96
- package/src/git.ts +0 -72
- package/src/metrics/flow.ts +0 -53
- package/src/metrics/index.ts +0 -169
- package/src/metrics/rework.ts +0 -45
- package/src/metrics/spirals.ts +0 -173
- package/src/metrics/trust.ts +0 -80
- package/src/metrics/velocity.ts +0 -86
- package/src/output/index.ts +0 -20
- package/src/output/json.ts +0 -51
- package/src/output/markdown.ts +0 -111
- package/src/output/terminal.ts +0 -109
- package/src/types.ts +0 -68
- package/tsconfig.json +0 -19
package/src/metrics/index.ts
DELETED
|
@@ -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';
|
package/src/metrics/rework.ts
DELETED
|
@@ -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
|
-
}
|
package/src/metrics/spirals.ts
DELETED
|
@@ -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
|
-
}
|
package/src/metrics/trust.ts
DELETED
|
@@ -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
|
-
}
|
package/src/metrics/velocity.ts
DELETED
|
@@ -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
|
-
}
|
package/src/output/index.ts
DELETED
|
@@ -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';
|
package/src/output/json.ts
DELETED
|
@@ -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
|
-
}
|