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:
parent
b45fdad911
commit
63506f7d5c
21 changed files with 841 additions and 13 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -69,6 +69,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- **`references/fold-in-loop.md` (A2-F9)** — the capture → classify → promote → enforce loop documented **domain-generally and in-tree**, so an adopter with no external writing contract has the whole loop (that absence "is not a gap" — the same mirror rule `editorial-reviewer` uses). Names the other four accumulation silos (specifics-bank, brain, voice drift log, A/B learnings) that Step 11 surfaces together, because a retro that empties one queue teaches the operator the others are empty too. references 28 → 29.
|
||||
- Suites: test-runner 197 → 217 (Section 16u: 18 unconditional greps + a non-vacuity self-test on the capture predicate; the phase-coverage sweep now spans **17** phases; anti-erosion floor 179 → 198). All nine suites green.
|
||||
|
||||
### Added — the do-next contract: a measurement changes the next piece (N15 / A2-F10, A2-F12, A2-F13, A2-F14)
|
||||
|
||||
- **`recordDoNext()` in `state-updater.mjs`** — one channel from measurement to creation. `/linkedin:report`, `/linkedin:analyze`, an `/linkedin:ab-test` **Adopt** verdict and the `post-feedback-monitor`'s 48h learning each persist 2–3 directives (imperative + evidence pointer, tagged with their source) to `## Do-Next Directives` in the state file; `post`/`quick`/`batch`/`create` Step 0 and `newsletter` Step 1 read them before drafting. Before this every analytics surface ended in chat (`grep do.next` across the repo returned 0) and the next drafting session — a fresh context — never saw any of it.
|
||||
- **Lifetime is replace-by-source + a 60-day age floor** — each writer supersedes only its own rows, so a new weekly report never evicts an A/B Adopt decision, and the section stays a live steering signal instead of a growing backlog. Deterministic, no AI in the write; `--record-do-next` exposes it as a CLI verb.
|
||||
- **Previous-edition calibration (A2-F14)** — `newsletter` Step 1 matches the previous edition to its measured numbers through the queue id (`drafts/queue.json` → `analytics/posts/*.json` by publish date, disambiguated on the hook preview), with an explicit **honest-miss** branch: an unmeasured edition is stated as unmeasured, never estimated.
|
||||
- test-runner 213 → 232 (Section 16v: 15 unconditional do-next greps + self-test; anti-erosion floor 198 → 213). Two real bugs found by the tests themselves: a section regex missing `/m` (old rows were never superseded or pruned) and fixtures dated 7 months before their own `today`, which had made the replace-by-source test falsely green.
|
||||
|
||||
### Added — measure-truth: reach split + honest boundary map (N16 / D-3, A2-F11, E#9)
|
||||
|
||||
- **Out-of-network reach as an optional manual column (D-3)** — LinkedIn split a post's impressions into in-network and out-of-network in June 2026 (post analytics → Discovery, under the impressions count): the first native number that says whether a post **acquired** audience or only resonated with the audience it already had. It is shown as a **percentage split** and is **absent from the CSV export** (whether it will ever be exported is unverified), so it follows the saves pattern — add an `Out-of-network` (or `In-network`) column with the percentage read off that panel and `/linkedin:import` ingests it.
|
||||
- **A separate percent parser, deliberately not the count parser** — `parseOptionalPercent()` reads a comma as a **decimal** mark (`36,5` → 36.5; `parseOptionalCount`'s US-thousands rule would have said 365) and **refuses any value above 100**: in one column an absolute impression count and a share are indistinguishable, so the honest answer is unknown rather than a guess. Blank, non-numeric and negative stay unknown; a genuine `0` is kept (nothing left the network).
|
||||
- **One stored half, cross-checked** — only `outOfNetworkPct` is stored, because the two halves describe one split and keeping both would let a record contradict itself. An `In-network` cell is stored as its complement, and a transcribed **pair that does not sum to ~100** (one point of rounding slack, since the UI rounds each half independently) is **discarded as unknown** instead of half-trusted.
|
||||
- **Impressions-weighted roll-ups** — `weightedOutOfNetworkPct()` feeds an optional `avgOutOfNetworkPct` on the weekly and monthly summaries. Weighted, because a flat mean lets a 50-impression post at 90 % outvote a 10,000-impression one at 20 %; posts without a reading are excluded rather than folded in as 0; zero total weight yields `undefined`, never 0 or NaN. Reach-free imports stay byte-identical to pre-N16 output, on screen and on disk.
|
||||
- **Not part of the engagement rate** — reach is distribution, not engagement, so it never enters `engagementRate` (which stays comparable to every historical import). `/linkedin:report` renders the split as **acquisition (out) vs resonance (in)**, states coverage honestly, and says "not entered for this period" rather than estimating an unentered split. A reach reading routes into the N15 do-next channel so it shapes the next post instead of being admired in the report.
|
||||
- **Operator-gated baseline updates (A2-F11)** — new `/linkedin:report` **Step 7c**: the report now offers a diff against the user's `audience-insights/engagement-patterns.md` baseline (the file `analytics-interpreter`, `strategy-advisor` and `/linkedin:audit` treat as tracked truth, and which nothing in the pipeline used to write, so it went stale silently). Old → new with the evidence, an explicit go via `AskUserQuestion`, changes labelled *provisional* when they rest on 1–2 posts, an append to the section's update log — and never a silent write, never into the plugin's shipped template.
|
||||
- **Boundary map brought current (E#9)** — the README boundaries now state all three honestly: dwell **explicitly unmeasurable** (internal to LinkedIn, no count to transcribe, no API), saves partner-gated (`POST_SAVE` on `/memberCreatorPostAnalytics` from v202604), and reach native-but-not-exported with export status **unverified**.
|
||||
- analytics suite 119 → 144 (25 new tests: percent contract, either-half resolution, split cross-check, weighted roll-up, backward-compat); test-runner 232 → 247 (Section 16w: 15 unconditional greps + self-test; anti-erosion floor 213 → 228); `tsc` clean.
|
||||
|
||||
### Built feedback (awaiting consumer-side proof)
|
||||
|
||||
- **MR-F9** (demand-sweep «innenfra og ut») — built; the (a)/(b)/(c) evidence gate is a runtime demonstration, proven consumer-side (plugin agents don't resolve in the dev repo).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue