linkedin-studio/scripts/analytics/src/reports/heatmap.ts
Kjell Tore Guttormsen 4dc868ea62 feat(ultraplan-local): v1.6.0 — /ultraresearch-local deep research command
Add /ultraresearch-local for structured research combining local codebase
analysis with external knowledge via parallel agent swarms. Produces research
briefs with triangulation, confidence ratings, and source quality assessment.

New command: /ultraresearch-local with modes --quick, --local, --external, --fg.
New agents: research-orchestrator (opus), docs-researcher, community-researcher,
security-researcher, contrarian-researcher, gemini-bridge (all sonnet).
New template: research-brief-template.md.

Integration: --research flag in /ultraplan-local accepts pre-built research
briefs (up to 3), enriches the interview and exploration phases. Planning
orchestrator cross-references brief findings during synthesis.

Design principle: Context Engineering — right information to right agent at
right time. Research briefs are structured artifacts in the pipeline:
ultraresearch → brief → ultraplan --research → plan → ultraexecute.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 08:58:35 +02:00

85 lines
2.7 KiB
TypeScript

import type { PostAnalytics, DayOfWeekMetrics, HeatmapReport } from "../models/types.js";
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
// Convert JS getDay() (0=Sun) to ISO weekday (1=Mon, 7=Sun)
function toISOWeekday(jsDay: number): number {
return jsDay === 0 ? 7 : jsDay;
}
/**
* Generate a day-of-week performance heatmap from post analytics data.
* Groups posts by day of week and calculates average metrics per day.
*/
export function generateHeatmap(posts: PostAnalytics[]): HeatmapReport {
// Initialize buckets for all 7 days (ISO: 1=Mon to 7=Sun)
const buckets: Map<number, PostAnalytics[]> = new Map();
for (let i = 1; i <= 7; i++) {
buckets.set(i, []);
}
// Group posts by ISO weekday
for (const post of posts) {
const jsDay = new Date(post.publishedDate).getUTCDay();
const isoDay = toISOWeekday(jsDay);
buckets.get(isoDay)!.push(post);
}
// Build metrics per day
const byDayOfWeek: DayOfWeekMetrics[] = [];
for (let isoDay = 1; isoDay <= 7; isoDay++) {
const dayPosts = buckets.get(isoDay)!;
const jsDay = isoDay === 7 ? 0 : isoDay;
const dayName = DAY_NAMES[jsDay];
if (dayPosts.length === 0) {
byDayOfWeek.push({
dayName,
dayIndex: isoDay,
postCount: 0,
avgImpressions: 0,
avgEngagementRate: 0,
});
continue;
}
const totalImpressions = dayPosts.reduce((sum, p) => sum + p.metrics.impressions, 0);
const totalEngagement = dayPosts.reduce((sum, p) => sum + p.metrics.engagementRate, 0);
const bestPost = dayPosts.reduce((best, p) =>
p.metrics.impressions > best.metrics.impressions ? p : best
);
byDayOfWeek.push({
dayName,
dayIndex: isoDay,
postCount: dayPosts.length,
avgImpressions: Math.round(totalImpressions / dayPosts.length),
avgEngagementRate: parseFloat((totalEngagement / dayPosts.length).toFixed(1)),
bestPost,
});
}
// Find best days
const daysWithPosts = byDayOfWeek.filter(d => d.postCount > 0);
const bestDayImpressions = daysWithPosts.length > 0
? daysWithPosts.reduce((best, d) => d.avgImpressions > best.avgImpressions ? d : best).dayName
: "N/A";
const bestDayEngagement = daysWithPosts.length > 0
? daysWithPosts.reduce((best, d) => d.avgEngagementRate > best.avgEngagementRate ? d : best).dayName
: "N/A";
// Date range
const sortedDates = posts.map(p => p.publishedDate).sort();
const dateRange = posts.length > 0
? { from: sortedDates[0], to: sortedDates[sortedDates.length - 1] }
: { from: "", to: "" };
return {
generatedAt: new Date().toISOString(),
postsAnalyzed: posts.length,
dateRange,
byDayOfWeek,
bestDayImpressions,
bestDayEngagement,
};
}