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>
85 lines
2.7 KiB
TypeScript
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,
|
|
};
|
|
}
|