feat(linkedin-studio): S16 — optional manual saves in analytics + close deferred onboarding Write MAJOR
Lifts the original v4.0.0 Non-Goal: an optional, manually-entered `saves`
metric through the analytics layer, built location-agnostic (option c) so
UI-brief §9b/M0 relocates the data dir in one place later.
- types: PostMetrics.saves? + Weekly/Monthly summary.totalSaves? (optional);
new RankableMetric type for the always-numeric index-access whitelist
- parser: dedicated parseOptionalCount() — blank/non-numeric/negative -> undefined
("unknown != 0"), genuine 0 kept; saves NOT folded into engagementRate
- reports: totalSaves set only when >=1 post carries saves (backward-compat)
- cli: saves surfaced in import summary + weekly/monthly totals + per-post
- S16-pre: onboarding.md allowed-tools gains Write (closes S15-deferred MAJOR)
- docs (three-doc rule): plugin README boundary + analytics README + root README
+ plugin CLAUDE.md + CHANGELOG; dwell stays explicitly unmeasurable
Independent /trekreview: brief-conformance 0 findings; code-correctness 2 MAJOR
(own lockstep misses) FIXED in-session (parseOptionalCount + edge tests). Gate:
tsc clean, analytics 116/116, lint 74/0/0, hooks 98/98. Within-v4.1.0 refinement
(no surface/count/version change).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e36efe9944
commit
69fec21f43
17 changed files with 412 additions and 117 deletions
|
|
@ -28,6 +28,12 @@ export function generateMonthlyReport(root: string, month: string): MonthlyRepor
|
|||
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);
|
||||
// Optional saves: present only when ≥1 post carries manual saves data —
|
||||
// keeps saves-free months byte-identical to pre-saves output (backward-compat).
|
||||
const savesPosts = monthPosts.filter(p => p.metrics.saves !== undefined);
|
||||
const totalSaves = savesPosts.length > 0
|
||||
? savesPosts.reduce((s, p) => s + (p.metrics.saves ?? 0), 0)
|
||||
: undefined;
|
||||
const avgEngagementRate = totalPosts > 0
|
||||
? parseFloat(mean(monthPosts.map(p => p.metrics.engagementRate)).toFixed(2))
|
||||
: 0;
|
||||
|
|
@ -101,6 +107,7 @@ export function generateMonthlyReport(root: string, month: string): MonthlyRepor
|
|||
totalComments,
|
||||
totalShares,
|
||||
totalClicks,
|
||||
...(totalSaves !== undefined ? { totalSaves } : {}),
|
||||
avgEngagementRate,
|
||||
avgImpressionsPerPost,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -154,12 +154,23 @@ export function generateWeeklyReport(analyticsRoot: string, week?: string): Week
|
|||
}
|
||||
|
||||
// Calculate summary metrics
|
||||
let totalSaves = 0;
|
||||
let sawSaves = false;
|
||||
for (const post of weekPosts) {
|
||||
report.summary.totalImpressions += post.metrics.impressions;
|
||||
report.summary.totalReactions += post.metrics.reactions;
|
||||
report.summary.totalComments += post.metrics.comments;
|
||||
report.summary.totalShares += post.metrics.shares;
|
||||
report.summary.totalClicks += post.metrics.clicks;
|
||||
if (post.metrics.saves !== undefined) {
|
||||
totalSaves += post.metrics.saves;
|
||||
sawSaves = true;
|
||||
}
|
||||
}
|
||||
// Only surface saves when at least one post carried it — keeps saves-free
|
||||
// reports byte-identical to pre-saves output (backward-compat).
|
||||
if (sawSaves) {
|
||||
report.summary.totalSaves = totalSaves;
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue