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>
This commit is contained in:
commit
40986575b6
150 changed files with 34216 additions and 0 deletions
117
scripts/analytics/src/reports/monthly.ts
Normal file
117
scripts/analytics/src/reports/monthly.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import type { PostAnalytics, MonthlyReport } from "../models/types.js";
|
||||
import { loadAllPosts, loadMonthlyReport, saveMonthlyReport } from "../utils/storage.js";
|
||||
import { mean } from "../utils/stats.js";
|
||||
import { detectAlerts } from "../utils/alerts.js";
|
||||
import { getISOWeek } from "./weekly.js";
|
||||
|
||||
/**
|
||||
* Get previous month string (e.g., "2026-03" → "2026-02")
|
||||
*/
|
||||
function getPreviousMonth(month: string): string {
|
||||
const [year, m] = month.split("-").map(Number);
|
||||
if (m === 1) return `${year - 1}-12`;
|
||||
return `${year}-${String(m - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a monthly report with optional MoM comparison.
|
||||
* Saves the report to disk and returns it.
|
||||
*/
|
||||
export function generateMonthlyReport(root: string, month: string): MonthlyReport {
|
||||
const allPosts = loadAllPosts(root);
|
||||
const monthPosts = allPosts.filter(p => p.publishedDate.startsWith(month));
|
||||
|
||||
// Summary
|
||||
const totalPosts = monthPosts.length;
|
||||
const totalImpressions = monthPosts.reduce((s, p) => s + p.metrics.impressions, 0);
|
||||
const totalReactions = monthPosts.reduce((s, p) => s + p.metrics.reactions, 0);
|
||||
const totalComments = monthPosts.reduce((s, p) => s + p.metrics.comments, 0);
|
||||
const totalShares = monthPosts.reduce((s, p) => s + p.metrics.shares, 0);
|
||||
const totalClicks = monthPosts.reduce((s, p) => s + p.metrics.clicks, 0);
|
||||
const avgEngagementRate = totalPosts > 0
|
||||
? parseFloat(mean(monthPosts.map(p => p.metrics.engagementRate)).toFixed(2))
|
||||
: 0;
|
||||
const avgImpressionsPerPost = totalPosts > 0
|
||||
? Math.round(totalImpressions / totalPosts)
|
||||
: 0;
|
||||
|
||||
// Top performers (sorted by impressions desc)
|
||||
const topPerformers = [...monthPosts]
|
||||
.sort((a, b) => b.metrics.impressions - a.metrics.impressions)
|
||||
.slice(0, 5);
|
||||
|
||||
// Weekly breakdown
|
||||
const weekBuckets = new Map<string, PostAnalytics[]>();
|
||||
for (const post of monthPosts) {
|
||||
const week = getISOWeek(new Date(post.publishedDate + "T00:00:00Z"));
|
||||
if (!weekBuckets.has(week)) weekBuckets.set(week, []);
|
||||
weekBuckets.get(week)!.push(post);
|
||||
}
|
||||
|
||||
const byWeek = Array.from(weekBuckets.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([week, posts]) => ({
|
||||
week,
|
||||
postCount: posts.length,
|
||||
avgImpressions: Math.round(mean(posts.map(p => p.metrics.impressions))),
|
||||
avgEngagementRate: parseFloat(mean(posts.map(p => p.metrics.engagementRate)).toFixed(1)),
|
||||
}));
|
||||
|
||||
// MoM comparison
|
||||
const prevMonth = getPreviousMonth(month);
|
||||
const prevReport = loadMonthlyReport(root, prevMonth);
|
||||
|
||||
let trends: MonthlyReport["trends"];
|
||||
if (prevReport && prevReport.summary.totalPosts > 0) {
|
||||
const pctImpr = prevReport.summary.totalImpressions > 0
|
||||
? parseFloat(((totalImpressions - prevReport.summary.totalImpressions) / prevReport.summary.totalImpressions * 100).toFixed(1))
|
||||
: null;
|
||||
const pctEng = prevReport.summary.avgEngagementRate > 0
|
||||
? parseFloat(((avgEngagementRate - prevReport.summary.avgEngagementRate) / prevReport.summary.avgEngagementRate * 100).toFixed(1))
|
||||
: null;
|
||||
const pctPosts = prevReport.summary.totalPosts > 0
|
||||
? parseFloat(((totalPosts - prevReport.summary.totalPosts) / prevReport.summary.totalPosts * 100).toFixed(1))
|
||||
: null;
|
||||
|
||||
trends = {
|
||||
comparedTo: prevMonth,
|
||||
percentChange: {
|
||||
impressions: pctImpr,
|
||||
engagement: pctEng,
|
||||
postCount: pctPosts,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
trends = {
|
||||
comparedTo: null,
|
||||
percentChange: { impressions: null, engagement: null, postCount: null },
|
||||
};
|
||||
}
|
||||
|
||||
// Alerts
|
||||
const alerts = totalPosts > 0 ? detectAlerts(monthPosts, "impressions") : [];
|
||||
|
||||
const report: MonthlyReport = {
|
||||
month,
|
||||
generatedAt: new Date().toISOString(),
|
||||
summary: {
|
||||
totalPosts,
|
||||
totalImpressions,
|
||||
totalReactions,
|
||||
totalComments,
|
||||
totalShares,
|
||||
totalClicks,
|
||||
avgEngagementRate,
|
||||
avgImpressionsPerPost,
|
||||
},
|
||||
topPerformers,
|
||||
byWeek,
|
||||
trends,
|
||||
alerts,
|
||||
};
|
||||
|
||||
// Save report
|
||||
saveMonthlyReport(root, report);
|
||||
|
||||
return report;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue