feat(linkedin-studio): N16 — out-of-network-andel + patterns-oppdatering + boundary-map [skip-docs]

Reach-splitten (in/out-of-network) er native i LinkedIns post-analytics siden juni
2026, men vises som PROSENT og finnes ikke i CSV-eksporten. Planen antok to
manuelle antall; verifiseringen viste prosent, så modellen er ett felt —
outOfNetworkPct — og in-network er komplementet.

- parseOptionalPercent: egen parser, ikke parseOptionalCount. Komma er desimal
  (36,5 -> 36.5, aldri 365), og verdi >100 avvises: i én kolonne kan ikke et
  absolutt antall skilles fra en andel, så svaret er unknown, ikke en gjetning.
  Blank/ikke-numerisk/negativ -> unknown; ekte 0 beholdes.
- Ett lagret halvpart, kryssjekket: In-network godtas og lagres som komplement;
  et transkribert par som ikke summerer til ~100 (±1 avrunding) forkastes som
  unknown i stedet for å bli halvveis trodd.
- weightedOutOfNetworkPct: impressions-vektet roll-up (avgOutOfNetworkPct, uke +
  måned). Flatt snitt lar en 50-visnings-post slå en på 10 000; poster uten
  avlesning ekskluderes, og null vekt gir undefined — aldri 0, aldri NaN.
- Reach inngår ALDRI i engagementRate (distribusjon != engasjement). Rapporten
  leser den som akvisisjon (ut) vs resonans (inn), og sier «ikke ført for denne
  perioden» framfor å estimere. En reach-innsikt går inn i N15s do-next-kanal.
- Step 7c (A2-F11): rapporten tilbyr diff mot brukerens engagement-patterns.md
  med eksplisitt go — aldri stille skriving, aldri inn i den shippede malen.
- Boundary-map (E#9): dwell eksplisitt umålbar, saves partner-gated, reach
  native men CSV-eksport uverifisert.
- Reach-frie importer er byte-identiske med før, på skjerm og på disk.

TDD: rødt bevist først (10 feilende), analytics 119 -> 144 tester, tsc ren.
test-runner 232 -> 247 (Section 16w, gulv 213 -> 228). Alle suiter grønne.
CHANGELOG: N15-oppføringen manglet og er backfilt sammen med N16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 15:56:04 +02:00
commit 63506f7d5c
21 changed files with 841 additions and 13 deletions

View file

@ -104,6 +104,31 @@ describe("generateMonthlyReport", () => {
assert.equal(report.summary.totalSaves, undefined);
});
test("rolls out-of-network shares up as an impressions-weighted average", () => {
const withReach = (p: PostAnalytics, outOfNetworkPct: number): PostAnalytics => ({
...p,
metrics: { ...p.metrics, outOfNetworkPct },
});
const posts: PostAnalytics[] = [
withReach(createPost("2026-03-03", 10000, 3.0), 20),
withReach(createPost("2026-03-05", 1000, 4.0), 80),
createPost("2026-03-10", 5000, 3.5), // no share entered — must not dilute
];
const root = setupTestRoot(posts);
const report = generateMonthlyReport(root, "2026-03");
assert.equal(
report.summary.avgOutOfNetworkPct,
25.5,
"Should weight by impressions (25.5), not average the shares flat (50)"
);
});
test("leaves avgOutOfNetworkPct undefined for reach-free months (backward-compat)", () => {
const root = setupTestRoot(marchPosts);
const report = generateMonthlyReport(root, "2026-03");
assert.equal(report.summary.avgOutOfNetworkPct, undefined);
});
test("generates weekly breakdown within month", () => {
const root = setupTestRoot(marchPosts);
const report = generateMonthlyReport(root, "2026-03");