repo-wrapped 0.0.7 → 0.0.9
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/.github/agents/complete.agent.md +257 -0
- package/.github/agents/feature-scaffold.agent.md +248 -0
- package/.github/agents/jsdoc.agent.md +243 -0
- package/.github/agents/plan.agent.md +202 -0
- package/.github/agents/spec-writer.agent.md +169 -0
- package/.github/agents/test-writer.agent.md +169 -0
- package/.stylelintrc.json +27 -0
- package/README.md +94 -94
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +446 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +446 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +7039 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/commands/generate.js +56 -56
- package/dist/config/defaults.js +158 -0
- package/dist/config/index.js +10 -0
- package/dist/features/achievements/data/achievements.json +284 -0
- package/dist/features/achievements/engine.js +140 -0
- package/dist/features/achievements/evaluators.js +246 -0
- package/dist/features/achievements/helpers.js +58 -0
- package/dist/features/achievements/index.js +57 -0
- package/dist/features/achievements/loader.js +88 -0
- package/dist/features/achievements/template.js +155 -0
- package/dist/features/achievements/types.js +7 -0
- package/dist/features/commit-quality/analyzer.js +378 -0
- package/dist/features/commit-quality/analyzer.test.js +484 -0
- package/dist/features/commit-quality/index.js +28 -0
- package/dist/features/commit-quality/template.js +114 -0
- package/dist/features/commit-quality/types.js +2 -0
- package/dist/features/comparison/analyzer.js +222 -0
- package/dist/features/comparison/index.js +28 -0
- package/dist/features/comparison/template.js +119 -0
- package/dist/features/comparison/types.js +2 -0
- package/dist/features/contribution-graph/index.js +9 -0
- package/dist/features/contribution-graph/template.js +89 -0
- package/dist/features/events/index.js +31 -0
- package/dist/features/events/parser.js +253 -0
- package/dist/features/events/template.js +113 -0
- package/dist/features/events/types.js +2 -0
- package/dist/features/executive-summary/generator.js +275 -0
- package/dist/features/executive-summary/index.js +27 -0
- package/dist/features/executive-summary/template.js +80 -0
- package/dist/features/executive-summary/types.js +2 -0
- package/dist/features/gaps/analyzer.js +298 -0
- package/dist/features/gaps/analyzer.test.js +517 -0
- package/dist/features/gaps/index.js +27 -0
- package/dist/features/gaps/template.js +190 -0
- package/dist/features/gaps/types.js +2 -0
- package/dist/features/impact/analyzer.js +248 -0
- package/dist/features/impact/index.js +26 -0
- package/dist/features/impact/template.js +118 -0
- package/dist/features/impact/types.js +2 -0
- package/dist/features/index.js +40 -0
- package/dist/features/knowledge/analyzer.js +385 -0
- package/dist/features/knowledge/index.js +26 -0
- package/dist/features/knowledge/template.js +239 -0
- package/dist/features/knowledge/types.js +2 -0
- package/dist/features/streaks/calculator.js +184 -0
- package/dist/features/streaks/calculator.test.js +366 -0
- package/dist/features/streaks/index.js +36 -0
- package/dist/features/streaks/template.js +41 -0
- package/dist/features/streaks/types.js +9 -0
- package/dist/features/team/analyzer.js +316 -0
- package/dist/features/team/index.js +30 -0
- package/dist/features/team/template.js +146 -0
- package/dist/features/team/types.js +2 -0
- package/dist/features/time-patterns/analyzer.js +319 -0
- package/dist/features/time-patterns/analyzer.test.js +278 -0
- package/dist/features/time-patterns/index.js +37 -0
- package/dist/features/time-patterns/template.js +109 -0
- package/dist/features/time-patterns/types.js +9 -0
- package/dist/features/velocity/analyzer.js +257 -0
- package/dist/features/velocity/analyzer.test.js +383 -0
- package/dist/features/velocity/index.js +27 -0
- package/dist/features/velocity/template.js +189 -0
- package/dist/features/velocity/types.js +2 -0
- package/dist/generators/html/scripts/knowledge.js +17 -0
- package/dist/generators/html/styles/base.css +8 -3
- package/dist/generators/html/styles/components.css +121 -1
- package/dist/generators/html/styles/knowledge.css +21 -0
- package/dist/generators/html/styles/leaddev.css +108 -48
- package/dist/generators/html/styles/strategic-insights.css +1337 -0
- package/dist/generators/html/templates/commitQualitySection.js +28 -2
- package/dist/generators/html/templates/executiveSummarySection.js +0 -4
- package/dist/generators/html/templates/impactSection.js +8 -6
- package/dist/generators/html/templates/knowledgeSection.js +16 -2
- package/dist/generators/html/templates/velocitySection.js +2 -2
- package/dist/generators/html/types.js +7 -0
- package/dist/generators/html/utils/analysisRunner.js +93 -0
- package/dist/generators/html/utils/cardBuilder.js +47 -0
- package/dist/generators/html/utils/contextBuilder.js +54 -0
- package/dist/generators/html/utils/htmlDocumentBuilder.js +396 -0
- package/dist/generators/html/utils/kpiBuilder.js +76 -0
- package/dist/generators/html/utils/sectionWrapper.js +71 -0
- package/dist/generators/html/utils/styleLoader.js +2 -2
- package/dist/html/analysisRunner.js +93 -0
- package/dist/html/htmlDocumentBuilder.js +396 -0
- package/dist/html/index.js +29 -0
- package/dist/html/shared/colorUtils.js +61 -0
- package/dist/html/shared/commitMapBuilder.js +23 -0
- package/dist/html/shared/components/cardBuilder.js +47 -0
- package/dist/html/shared/components/index.js +18 -0
- package/dist/html/shared/components/kpiBuilder.js +76 -0
- package/dist/html/shared/components/sectionWrapper.js +71 -0
- package/dist/html/shared/contextBuilder.js +54 -0
- package/dist/html/shared/dateRangeCalculator.js +56 -0
- package/dist/html/shared/developerStatsCalculator.js +28 -0
- package/dist/html/shared/index.js +39 -0
- package/dist/html/shared/scriptLoader.js +15 -0
- package/dist/html/shared/scripts/export.js +125 -0
- package/dist/html/shared/scripts/knowledge.js +137 -0
- package/dist/html/shared/scripts/modal.js +68 -0
- package/dist/html/shared/scripts/navigation.js +156 -0
- package/dist/html/shared/scripts/tabs.js +18 -0
- package/dist/html/shared/scripts/tooltip.js +21 -0
- package/dist/html/shared/styleLoader.js +18 -0
- package/dist/html/shared/styles/achievements.css +387 -0
- package/dist/html/shared/styles/base.css +822 -0
- package/dist/html/shared/styles/components.css +1511 -0
- package/dist/html/shared/styles/knowledge.css +242 -0
- package/dist/html/shared/styles/strategic-insights.css +1337 -0
- package/dist/html/shared/weekGrouper.js +27 -0
- package/dist/html/types.js +7 -0
- package/dist/index.js +39 -39
- package/dist/test/helpers/commitFactory.js +166 -0
- package/dist/test/helpers/dateUtils.js +101 -0
- package/dist/test/helpers/index.js +29 -0
- package/dist/test/setup.js +17 -0
- package/dist/test/smoke.test.js +94 -0
- package/dist/types/achievements.js +7 -0
- package/dist/types/analysis.js +7 -0
- package/dist/types/core.js +7 -0
- package/dist/types/index.js +38 -0
- package/dist/types/options.js +7 -0
- package/dist/types/shared.js +7 -0
- package/dist/types/strategic.js +7 -0
- package/dist/types/summary.js +7 -0
- package/dist/utils/achievementDefinitions.js +22 -22
- package/dist/utils/analyzerContextBuilder.js +124 -0
- package/dist/utils/commitQualityAnalyzer.js +13 -2
- package/dist/utils/emptyResults.js +95 -0
- package/dist/utils/fileHotspotAnalyzer.js +4 -12
- package/dist/utils/gapAnalyzer.js +26 -28
- package/dist/utils/gitParser.test.js +363 -0
- package/dist/utils/htmlGenerator.js +62 -466
- package/dist/utils/impactAnalyzer.js +20 -19
- package/dist/utils/knowledgeDistributionAnalyzer.js +32 -27
- package/dist/utils/matrixGenerator.js +13 -13
- package/dist/utils/rangeComparisonAnalyzer.js +2 -2
- package/dist/utils/streakCalculator.js +77 -27
- package/dist/utils/teamAnalyzer.js +20 -1
- package/dist/utils/timePatternAnalyzer.js +18 -3
- package/dist/utils/velocityAnalyzer.js +23 -18
- package/dist/utils/wrappedGenerator.js +8 -8
- package/package.json +74 -64
- package/vitest.config.ts +46 -0
- package/SPECS.md +0 -490
- package/dist/cli.js +0 -24
- package/dist/commands/index.js +0 -24
- package/test-team.txt +0 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildExecutiveSummarySection = buildExecutiveSummarySection;
|
|
4
|
+
const date_fns_1 = require("date-fns");
|
|
5
|
+
function buildExecutiveSummarySection(summary) {
|
|
6
|
+
return `
|
|
7
|
+
<div class="executive-summary-section">
|
|
8
|
+
<div class="executive-header">
|
|
9
|
+
<h2>📊 Executive Summary</h2>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="kpi-grid">
|
|
13
|
+
${summary.kpis.map(kpi => buildKPICard(kpi)).join('')}
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="insights-risks-grid">
|
|
17
|
+
<div class="insights-column">
|
|
18
|
+
<h3>💡 Key Insights</h3>
|
|
19
|
+
<div class="insights-list">
|
|
20
|
+
${summary.keyInsights.map(insight => `
|
|
21
|
+
<div class="insight-item ${insight.type}">
|
|
22
|
+
<span class="insight-icon">${insight.type === 'positive' ? '✅' : insight.type === 'negative' ? '⚠️' : 'ℹ️'}</span>
|
|
23
|
+
<span class="insight-text">${insight.insight}</span>
|
|
24
|
+
</div>
|
|
25
|
+
`).join('')}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="risks-column">
|
|
30
|
+
<h3>🚨 Risk Assessment</h3>
|
|
31
|
+
<div class="risks-list">
|
|
32
|
+
${summary.risks.length > 0 ? summary.risks.map(risk => `
|
|
33
|
+
<div class="risk-item ${risk.level}">
|
|
34
|
+
<span class="risk-badge">${risk.level.toUpperCase()}</span>
|
|
35
|
+
<div class="risk-content">
|
|
36
|
+
<span class="risk-category">${risk.category}</span>
|
|
37
|
+
<span class="risk-description">${risk.description}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
`).join('') : '<div class="no-risks">No significant risks identified</div>'}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="recommendations-section">
|
|
46
|
+
<h3>📋 Recommendations</h3>
|
|
47
|
+
<ol class="recommendations-list">
|
|
48
|
+
${summary.recommendations.map(rec => `<li>${rec}</li>`).join('')}
|
|
49
|
+
</ol>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="summary-footer">
|
|
53
|
+
Generated: ${(0, date_fns_1.format)(summary.generatedAt, 'MMM d, yyyy HH:mm')}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
function buildKPICard(kpi) {
|
|
59
|
+
const statusColors = {
|
|
60
|
+
green: '#69db7c',
|
|
61
|
+
yellow: '#ffd43b',
|
|
62
|
+
red: '#ff6b6b',
|
|
63
|
+
};
|
|
64
|
+
const trendArrow = kpi.trend === 'up' ? '↑' : kpi.trend === 'down' ? '↓' : '→';
|
|
65
|
+
const trendClass = kpi.trend === 'up' ? 'positive' : kpi.trend === 'down' ? 'negative' : 'neutral';
|
|
66
|
+
return `
|
|
67
|
+
<div class="kpi-card" style="--status-color: ${statusColors[kpi.status]}">
|
|
68
|
+
<div class="kpi-status-indicator" title="${kpi.status}"></div>
|
|
69
|
+
<div class="kpi-content">
|
|
70
|
+
<div class="kpi-label">${kpi.name}</div>
|
|
71
|
+
<div class="kpi-value">${kpi.value}</div>
|
|
72
|
+
<div class="kpi-trend ${trendClass}">
|
|
73
|
+
<span class="trend-arrow">${trendArrow}</span>
|
|
74
|
+
<span class="trend-value">${kpi.trendValue}</span>
|
|
75
|
+
</div>
|
|
76
|
+
${kpi.benchmark ? `<div class="kpi-benchmark">${kpi.benchmark}</div>` : ''}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeGaps = analyzeGaps;
|
|
4
|
+
exports.formatGapInsights = formatGapInsights;
|
|
5
|
+
const date_fns_1 = require("date-fns");
|
|
6
|
+
const emptyResults_1 = require("../../utils/emptyResults");
|
|
7
|
+
const analyzerContextBuilder_1 = require("../../utils/analyzerContextBuilder");
|
|
8
|
+
function analyzeGaps(ctxOrCommits, startDate, endDate, thresholdDays = 3, excludeWeekends = false) {
|
|
9
|
+
// Normalize to internal variables
|
|
10
|
+
let commits;
|
|
11
|
+
let start;
|
|
12
|
+
let end;
|
|
13
|
+
let threshold;
|
|
14
|
+
let skipWeekends;
|
|
15
|
+
if ((0, analyzerContextBuilder_1.isAnalyzerContext)(ctxOrCommits)) {
|
|
16
|
+
commits = ctxOrCommits.commits;
|
|
17
|
+
start = ctxOrCommits.dateRange.start;
|
|
18
|
+
end = ctxOrCommits.dateRange.end;
|
|
19
|
+
threshold = (0, analyzerContextBuilder_1.getOption)(ctxOrCommits, 'thresholdDays', 3);
|
|
20
|
+
skipWeekends = (0, analyzerContextBuilder_1.getOption)(ctxOrCommits, 'excludeWeekends', false);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
commits = ctxOrCommits;
|
|
24
|
+
start = startDate;
|
|
25
|
+
end = endDate;
|
|
26
|
+
threshold = thresholdDays;
|
|
27
|
+
skipWeekends = excludeWeekends;
|
|
28
|
+
}
|
|
29
|
+
if (commits.length === 0) {
|
|
30
|
+
return (0, emptyResults_1.getEmptyGapAnalysis)(start, end);
|
|
31
|
+
}
|
|
32
|
+
// Sort commits by date
|
|
33
|
+
const sortedCommits = [...commits].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
34
|
+
// Find all gaps
|
|
35
|
+
const gaps = findGaps(sortedCommits, start, end, threshold, skipWeekends);
|
|
36
|
+
// Calculate statistics
|
|
37
|
+
const totalGapDays = gaps.reduce((sum, gap) => sum + gap.durationDays, 0);
|
|
38
|
+
const totalPeriodDays = (0, date_fns_1.differenceInDays)(end, start) + 1;
|
|
39
|
+
const percentageOfPeriodInGaps = totalPeriodDays > 0
|
|
40
|
+
? Math.round((totalGapDays / totalPeriodDays) * 1000) / 10
|
|
41
|
+
: 0;
|
|
42
|
+
const longestGap = gaps.length > 0
|
|
43
|
+
? gaps.reduce((max, gap) => gap.durationDays > max.durationDays ? gap : max, gaps[0])
|
|
44
|
+
: null;
|
|
45
|
+
const averageGapLength = gaps.length > 0
|
|
46
|
+
? Math.round((totalGapDays / gaps.length) * 10) / 10
|
|
47
|
+
: 0;
|
|
48
|
+
// Calculate gap frequency (gaps per month)
|
|
49
|
+
const months = totalPeriodDays / 30;
|
|
50
|
+
const gapFrequency = months > 0 ? Math.round((gaps.length / months) * 10) / 10 : 0;
|
|
51
|
+
// Detect patterns
|
|
52
|
+
const patterns = detectGapPatterns(gaps);
|
|
53
|
+
// Assess risk
|
|
54
|
+
const { riskLevel, riskFactors } = assessGapRisk(gaps, percentageOfPeriodInGaps, longestGap);
|
|
55
|
+
return {
|
|
56
|
+
gaps,
|
|
57
|
+
totalGapDays,
|
|
58
|
+
percentageOfPeriodInGaps,
|
|
59
|
+
longestGap,
|
|
60
|
+
averageGapLength,
|
|
61
|
+
gapFrequency,
|
|
62
|
+
patterns,
|
|
63
|
+
riskLevel,
|
|
64
|
+
riskFactors,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function findGaps(sortedCommits, startDate, endDate, thresholdDays, excludeWeekends) {
|
|
68
|
+
const gaps = [];
|
|
69
|
+
// Check gap from startDate to first commit
|
|
70
|
+
const firstCommitDate = new Date(sortedCommits[0].date);
|
|
71
|
+
const daysToFirstCommit = calculateDaysBetween(startDate, firstCommitDate, excludeWeekends);
|
|
72
|
+
if (daysToFirstCommit >= thresholdDays) {
|
|
73
|
+
gaps.push({
|
|
74
|
+
start: startDate,
|
|
75
|
+
end: (0, date_fns_1.addDays)(firstCommitDate, -1),
|
|
76
|
+
durationDays: daysToFirstCommit,
|
|
77
|
+
commitBefore: null,
|
|
78
|
+
commitAfter: {
|
|
79
|
+
date: firstCommitDate,
|
|
80
|
+
message: sortedCommits[0].message,
|
|
81
|
+
author: sortedCommits[0].author,
|
|
82
|
+
},
|
|
83
|
+
possibleType: classifyGap(startDate, (0, date_fns_1.addDays)(firstCommitDate, -1), daysToFirstCommit),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Check gaps between commits
|
|
87
|
+
for (let i = 0; i < sortedCommits.length - 1; i++) {
|
|
88
|
+
const currentCommit = sortedCommits[i];
|
|
89
|
+
const nextCommit = sortedCommits[i + 1];
|
|
90
|
+
const currentDate = new Date(currentCommit.date);
|
|
91
|
+
const nextDate = new Date(nextCommit.date);
|
|
92
|
+
const daysBetween = calculateDaysBetween(currentDate, nextDate, excludeWeekends);
|
|
93
|
+
if (daysBetween >= thresholdDays) {
|
|
94
|
+
const gapStart = (0, date_fns_1.addDays)(currentDate, 1);
|
|
95
|
+
const gapEnd = (0, date_fns_1.addDays)(nextDate, -1);
|
|
96
|
+
gaps.push({
|
|
97
|
+
start: gapStart,
|
|
98
|
+
end: gapEnd,
|
|
99
|
+
durationDays: daysBetween,
|
|
100
|
+
commitBefore: {
|
|
101
|
+
date: currentDate,
|
|
102
|
+
message: currentCommit.message,
|
|
103
|
+
author: currentCommit.author,
|
|
104
|
+
},
|
|
105
|
+
commitAfter: {
|
|
106
|
+
date: nextDate,
|
|
107
|
+
message: nextCommit.message,
|
|
108
|
+
author: nextCommit.author,
|
|
109
|
+
},
|
|
110
|
+
possibleType: classifyGap(gapStart, gapEnd, daysBetween),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Check gap from last commit to endDate
|
|
115
|
+
const lastCommit = sortedCommits[sortedCommits.length - 1];
|
|
116
|
+
const lastCommitDate = new Date(lastCommit.date);
|
|
117
|
+
const daysAfterLast = calculateDaysBetween(lastCommitDate, endDate, excludeWeekends);
|
|
118
|
+
if (daysAfterLast >= thresholdDays) {
|
|
119
|
+
gaps.push({
|
|
120
|
+
start: (0, date_fns_1.addDays)(lastCommitDate, 1),
|
|
121
|
+
end: endDate,
|
|
122
|
+
durationDays: daysAfterLast,
|
|
123
|
+
commitBefore: {
|
|
124
|
+
date: lastCommitDate,
|
|
125
|
+
message: lastCommit.message,
|
|
126
|
+
author: lastCommit.author,
|
|
127
|
+
},
|
|
128
|
+
commitAfter: null,
|
|
129
|
+
possibleType: classifyGap((0, date_fns_1.addDays)(lastCommitDate, 1), endDate, daysAfterLast),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return gaps;
|
|
133
|
+
}
|
|
134
|
+
function calculateDaysBetween(start, end, excludeWeekends) {
|
|
135
|
+
if (!excludeWeekends) {
|
|
136
|
+
return (0, date_fns_1.differenceInDays)(end, start);
|
|
137
|
+
}
|
|
138
|
+
let days = 0;
|
|
139
|
+
let current = new Date(start);
|
|
140
|
+
while (current < end) {
|
|
141
|
+
if (!(0, date_fns_1.isWeekend)(current)) {
|
|
142
|
+
days++;
|
|
143
|
+
}
|
|
144
|
+
current = (0, date_fns_1.addDays)(current, 1);
|
|
145
|
+
}
|
|
146
|
+
return days;
|
|
147
|
+
}
|
|
148
|
+
function classifyGap(start, end, durationDays) {
|
|
149
|
+
// Check if it's just a weekend
|
|
150
|
+
if (durationDays <= 2) {
|
|
151
|
+
const startIsWeekend = (0, date_fns_1.isWeekend)(start);
|
|
152
|
+
const endIsWeekend = (0, date_fns_1.isWeekend)(end);
|
|
153
|
+
if (startIsWeekend || endIsWeekend) {
|
|
154
|
+
return 'weekend';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Check for common holiday periods (rough heuristics)
|
|
158
|
+
const startMonth = start.getMonth();
|
|
159
|
+
const startDay = start.getDate();
|
|
160
|
+
// Christmas/New Year period
|
|
161
|
+
if ((startMonth === 11 && startDay >= 20) || (startMonth === 0 && startDay <= 5)) {
|
|
162
|
+
return 'holiday';
|
|
163
|
+
}
|
|
164
|
+
// Summer vacation period (July-August) with longer gaps
|
|
165
|
+
if ((startMonth === 6 || startMonth === 7) && durationDays >= 5) {
|
|
166
|
+
return 'vacation';
|
|
167
|
+
}
|
|
168
|
+
// Longer gaps are more likely blockers
|
|
169
|
+
if (durationDays >= 7) {
|
|
170
|
+
return 'blocker';
|
|
171
|
+
}
|
|
172
|
+
return 'unknown';
|
|
173
|
+
}
|
|
174
|
+
function detectGapPatterns(gaps) {
|
|
175
|
+
const patterns = [];
|
|
176
|
+
if (gaps.length < 2) {
|
|
177
|
+
return patterns;
|
|
178
|
+
}
|
|
179
|
+
// Check for recurring patterns
|
|
180
|
+
const gapsByDayOfWeek = new Map();
|
|
181
|
+
for (const gap of gaps) {
|
|
182
|
+
const dayOfWeek = gap.start.getDay();
|
|
183
|
+
gapsByDayOfWeek.set(dayOfWeek, (gapsByDayOfWeek.get(dayOfWeek) || 0) + 1);
|
|
184
|
+
}
|
|
185
|
+
// Check if gaps tend to start on specific days (e.g., Mondays = after weekend extension)
|
|
186
|
+
for (const [day, count] of gapsByDayOfWeek) {
|
|
187
|
+
if (count >= 3 && count >= gaps.length * 0.4) {
|
|
188
|
+
const dayName = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][day];
|
|
189
|
+
patterns.push({
|
|
190
|
+
type: 'recurring-weekly',
|
|
191
|
+
description: `Gaps frequently start on ${dayName}s`,
|
|
192
|
+
occurrences: count,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Check for blocker pattern (multiple unknown/blocker gaps)
|
|
197
|
+
const blockerGaps = gaps.filter(g => g.possibleType === 'blocker' || g.possibleType === 'unknown');
|
|
198
|
+
if (blockerGaps.length >= 3) {
|
|
199
|
+
patterns.push({
|
|
200
|
+
type: 'random',
|
|
201
|
+
description: `${blockerGaps.length} unexplained gaps detected - potential recurring blockers`,
|
|
202
|
+
occurrences: blockerGaps.length,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return patterns;
|
|
206
|
+
}
|
|
207
|
+
function assessGapRisk(gaps, percentageInGaps, longestGap) {
|
|
208
|
+
const riskFactors = [];
|
|
209
|
+
let riskScore = 0;
|
|
210
|
+
// Factor 1: Percentage of time in gaps
|
|
211
|
+
if (percentageInGaps >= 30) {
|
|
212
|
+
riskScore += 3;
|
|
213
|
+
riskFactors.push(`${percentageInGaps}% of time spent in activity gaps`);
|
|
214
|
+
}
|
|
215
|
+
else if (percentageInGaps >= 20) {
|
|
216
|
+
riskScore += 2;
|
|
217
|
+
riskFactors.push(`${percentageInGaps}% of time spent in activity gaps`);
|
|
218
|
+
}
|
|
219
|
+
else if (percentageInGaps >= 10) {
|
|
220
|
+
riskScore += 1;
|
|
221
|
+
riskFactors.push(`${percentageInGaps}% of time spent in activity gaps`);
|
|
222
|
+
}
|
|
223
|
+
// Factor 2: Longest gap duration
|
|
224
|
+
if (longestGap) {
|
|
225
|
+
if (longestGap.durationDays >= 21) {
|
|
226
|
+
riskScore += 3;
|
|
227
|
+
riskFactors.push(`Longest gap: ${longestGap.durationDays} days (3+ weeks)`);
|
|
228
|
+
}
|
|
229
|
+
else if (longestGap.durationDays >= 14) {
|
|
230
|
+
riskScore += 2;
|
|
231
|
+
riskFactors.push(`Longest gap: ${longestGap.durationDays} days (2+ weeks)`);
|
|
232
|
+
}
|
|
233
|
+
else if (longestGap.durationDays >= 7) {
|
|
234
|
+
riskScore += 1;
|
|
235
|
+
riskFactors.push(`Longest gap: ${longestGap.durationDays} days (1+ week)`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Factor 3: Number of blocker-type gaps
|
|
239
|
+
const blockerGaps = gaps.filter(g => g.possibleType === 'blocker');
|
|
240
|
+
if (blockerGaps.length >= 5) {
|
|
241
|
+
riskScore += 2;
|
|
242
|
+
riskFactors.push(`${blockerGaps.length} potential blocker gaps identified`);
|
|
243
|
+
}
|
|
244
|
+
else if (blockerGaps.length >= 2) {
|
|
245
|
+
riskScore += 1;
|
|
246
|
+
riskFactors.push(`${blockerGaps.length} potential blocker gaps identified`);
|
|
247
|
+
}
|
|
248
|
+
// Determine risk level
|
|
249
|
+
let riskLevel;
|
|
250
|
+
if (riskScore >= 6) {
|
|
251
|
+
riskLevel = 'critical';
|
|
252
|
+
}
|
|
253
|
+
else if (riskScore >= 4) {
|
|
254
|
+
riskLevel = 'high';
|
|
255
|
+
}
|
|
256
|
+
else if (riskScore >= 2) {
|
|
257
|
+
riskLevel = 'medium';
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
riskLevel = 'low';
|
|
261
|
+
}
|
|
262
|
+
if (riskFactors.length === 0) {
|
|
263
|
+
riskFactors.push('No significant gap patterns detected');
|
|
264
|
+
}
|
|
265
|
+
return { riskLevel, riskFactors };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Format gap analysis for display
|
|
269
|
+
*/
|
|
270
|
+
function formatGapInsights(analysis) {
|
|
271
|
+
const insights = [];
|
|
272
|
+
if (analysis.gaps.length === 0) {
|
|
273
|
+
insights.push('✅ No significant activity gaps detected');
|
|
274
|
+
return insights;
|
|
275
|
+
}
|
|
276
|
+
// Summary
|
|
277
|
+
insights.push(`🕳️ ${analysis.gaps.length} activity gap(s) detected`);
|
|
278
|
+
insights.push(`📅 ${analysis.totalGapDays} total gap days (${analysis.percentageOfPeriodInGaps}% of period)`);
|
|
279
|
+
// Longest gap
|
|
280
|
+
if (analysis.longestGap) {
|
|
281
|
+
const startStr = (0, date_fns_1.format)(analysis.longestGap.start, 'MMM d');
|
|
282
|
+
const endStr = (0, date_fns_1.format)(analysis.longestGap.end, 'MMM d, yyyy');
|
|
283
|
+
insights.push(`⏱️ Longest gap: ${analysis.longestGap.durationDays} days (${startStr} - ${endStr})`);
|
|
284
|
+
}
|
|
285
|
+
// Risk assessment
|
|
286
|
+
const riskEmoji = {
|
|
287
|
+
low: '🟢',
|
|
288
|
+
medium: '🟡',
|
|
289
|
+
high: '🟠',
|
|
290
|
+
critical: '🔴',
|
|
291
|
+
}[analysis.riskLevel];
|
|
292
|
+
insights.push(`${riskEmoji} Risk level: ${analysis.riskLevel.toUpperCase()}`);
|
|
293
|
+
// Top risk factors
|
|
294
|
+
for (const factor of analysis.riskFactors.slice(0, 3)) {
|
|
295
|
+
insights.push(` • ${factor}`);
|
|
296
|
+
}
|
|
297
|
+
return insights;
|
|
298
|
+
}
|