From b89868e3b1e256c2fb5cc7bab1ec3f8b6e6e54c7 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Mon, 22 Jun 2026 13:36:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(linkedin-studio):=20research-engine=20conf?= =?UTF-8?q?ig=20layer=20=E2=80=94=20sources=20+=20scoring=20modes=20+=20MC?= =?UTF-8?q?P=20profile=20(=C2=A75=20slice=202a)=20[skip-docs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declaration/config groundwork that slice 2b's trend-spotter upgrade reads. Standalone (no agent wiring yet — that's 2b), mirroring slice 1's pattern; [skip-docs] for the same reason slice 1 was — user-facing docs land when 2b wires the engine live. - references/trend-scoring-modes.md: methodology SSOT for two rubrics — kortform (feed post, timing 20%) + long-form (chronicle, depth 25% / timing 10%, per tema-research-motor-spec §4.2). Both sum to 100%. trend-spotter renders from this in 2b instead of inlining a matrix (S12-consistent). - config/trends-sources.template.md: shipped generic source-list defaults → user override at ${LINKEDIN_STUDIO_DATA}/trends/sources.md (data-dir, survives reinstall; same template->data-dir pattern as user-profile). - user-profile.template.md: new "Research Tooling" section — declared research MCPs (Tavily/Gemini/Perplexity/Other) + WebSearch/WebFetch floor. 2b routes MCP-first. - setup.md Step 3f + onboarding.md Phase 2: ask "which research MCPs?" -> profile. Store only what the user declares; no hard-coded MCP names. - test-runner.sh: EXPECT_REFS 26->27; generalized the M0 +1 delta-guard into a named-post-M0-additions guard (POSTM0_REFS) so a legit later ref doc passes while the anti-masking intent holds. Gate green 84/0/0. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY --- commands/onboarding.md | 1 + commands/setup.md | 9 ++- config/trends-sources.template.md | 76 +++++++++++++++++++++++ config/user-profile.template.md | 21 +++++++ references/trend-scoring-modes.md | 100 ++++++++++++++++++++++++++++++ scripts/test-runner.sh | 29 ++++++--- 6 files changed, 225 insertions(+), 11 deletions(-) create mode 100644 config/trends-sources.template.md create mode 100644 references/trend-scoring-modes.md diff --git a/commands/onboarding.md b/commands/onboarding.md index 58f88aa..99a31c2 100644 --- a/commands/onboarding.md +++ b/commands/onboarding.md @@ -154,6 +154,7 @@ file must contain no ``. 3. Job title / role 4. 5 expertise areas (these become your content pillars) 5. Target audience description +6. Research MCPs connected (Tavily / Gemini deep research / Perplexity — or "none"; WebSearch + WebFetch are the always-available floor). Store only what they name — don't invent MCP names. Save to `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/user-profile.md`. diff --git a/commands/setup.md b/commands/setup.md index 3cb8e5d..a0050ff 100644 --- a/commands/setup.md +++ b/commands/setup.md @@ -336,8 +336,13 @@ Guide through each section of the profile: - "Current follower count?" - "90-day growth goal?" -7. Read `config/user-profile.template.md` for structure -8. Write the completed profile to `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/user-profile.md` +7. **Research tooling:** + - "Which research MCPs do you have connected? (e.g., Tavily, Gemini deep research, Perplexity — or none)" + - Record exactly what they name. If they have none, that's fine — WebSearch + WebFetch are the always-available floor. + - This populates the **Research Tooling** section of the profile; the trend/research engine routes to a declared MCP first and falls back to the floor. Do not invent MCP names — store only what the user declares. + +8. Read `config/user-profile.template.md` for structure +9. Write the completed profile to `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/user-profile.md` **Important:** This file is gitignored (`.local.md` pattern), so personal data stays private. diff --git a/config/trends-sources.template.md b/config/trends-sources.template.md new file mode 100644 index 0000000..a6fa997 --- /dev/null +++ b/config/trends-sources.template.md @@ -0,0 +1,76 @@ +# Trend Sources (template) + +The **source list** a research-engine pass polls for trend/topic candidates. This file +ships **generic, niche-agnostic defaults** (source *categories*, not one person's beat). +Override it with your own list — the niche specifics (your vendors, your regulators, your +country's outlets) belong in the override, never here. + +## How the override works + +Copy this template into your per-user data dir and edit the copy: + +```bash +mkdir -p "${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends" +cp config/trends-sources.template.md \ + "${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends/sources.md" +``` + +A pass reads `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends/sources.md` +if it exists, and falls back to these shipped defaults otherwise. The override lives in the +data dir (next to the trend store `trends/trends.json`), so it **survives plugin +upgrades and reinstalls**. `LINKEDIN_STUDIO_DATA` overrides the root. + +Format below is read by a person and an agent both: a markdown list, one source per line, +grouped by tier. Keep `Name — URL — note` so a poll can cite the URL. + +--- + +## Tier 1 — Primary / breaking (poll daily, react within 24–48h) + +*First-party announcements and authoritative decisions in your field.* + +- [Major vendor blog] — [url] — releases & announcements +- [Major vendor blog] — [url] — releases & announcements +- [Regulator / governing body] — [url] — decisions & guidance + +## Tier 2 — Analysis & research (poll 2–3×/week, post within a week) + +*Where a development gets interpreted, not just reported.* + +- [Respected analysis publication] — [url] +- [Analyst house report stream] — [url] — adoption / market reports +- [Research index] — [url] — preprints / papers in your area + +## Tier 3 — Community signals (poll weekly, post if a pattern emerges) + +*Where practitioners surface what actually matters before the press does.* + +- [Practitioner forum / aggregator] — [url] +- [Topic-specific community] — [url] +- [Platform-native trending] — [url] + +## Tier 4 — Niche & seasonal (poll monthly, plan ahead) + +*Slower-moving sources with predictable cadence.* + +- [Key conference series] — [url] — announcement / agenda cycles +- [Earnings / report calendar] — [url] — scheduled releases +- Seasonal themes: [Q1 …] · [Q2 …] · [Q3 …] · [Q4 …] + +--- + +## Your niche additions + +Add the sources specific to your field below — these are the ones the generic defaults +above cannot know. (Example shape for a Norwegian public-sector / AI niche: national +digitalisation agency, data-protection authority, the relevant ministries, the local +tech press — replace with yours.) + +- [Your source] — [url] — [why it matters to your audience] +- [Your source] — [url] — [why it matters to your audience] + +--- + +*Tip: the [8 universal angles](../references/content-angles.md) and the scoring rubric +in [trend-scoring-modes.md](../references/trend-scoring-modes.md) decide what to DO with a +candidate once a poll surfaces it. This file only decides WHERE to look.* diff --git a/config/user-profile.template.md b/config/user-profile.template.md index d75e941..84e984d 100644 --- a/config/user-profile.template.md +++ b/config/user-profile.template.md @@ -116,6 +116,27 @@ cp config/user-profile.template.md config/user-profile.local.md --- +### Research Tooling + +**Which research MCPs do you have available?** The trend / research engine routes to a +declared MCP **first** and falls back to the always-available floor when none is declared. +Check what you actually have connected — leave the rest unchecked. (Names are examples; +add whatever you have under "Other" — nothing here is hard-coded downstream.) + +- [ ] Tavily (`tavily_search` / `tavily_research`) +- [ ] Gemini deep research (`gemini_deep_research`) +- [ ] Perplexity +- [ ] Other: [name the MCP + what it's good for] + +**Always-available floor (no MCP needed):** WebSearch + WebFetch. These are used when no +research MCP is declared above. (Note: WebSearch is US-biased — a research MCP gives +better coverage for non-US / regional sources.) + +**Preferred order (optional):** [e.g. "Tavily for niche/regional, Gemini for deep dives, +WebSearch as fallback" — or leave blank to let the engine pick] + +--- + ### Asset Utilization Preferences **When creating content, Claude should:** diff --git a/references/trend-scoring-modes.md b/references/trend-scoring-modes.md new file mode 100644 index 0000000..1044648 --- /dev/null +++ b/references/trend-scoring-modes.md @@ -0,0 +1,100 @@ +# Trend Scoring Modes Reference + +**Single source of truth** for how a discovered trend/topic candidate is scored. +There are **two modes** — they share the same 1–10 per-dimension scale and the same +composite formula, but they weight the dimensions differently because a feed post and +a long-form chronicle reward different things. Surfaces (the `trend-spotter` agent, any +research-engine pass) **select a mode and apply the matching rubric** — they do not +restate the weights inline. Cite this file; do not duplicate it. + +This file defines **methodology** (the weights), not user preference. The *source list* +a pass polls is separate and user-overridable — see +`config/trends-sources.template.md` → `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends/sources.md`. + +## How to read this file + +- Score each dimension **1–10** (see the bands in the per-mode tables). +- Composite = the weighted sum of the five dimension scores. Each mode's weights sum to + **100 %**, so the composite stays on the same 0–10 scale across modes. +- The **ordering** of the weights is the signal; the exact percentages are a deliberate, + documented choice, not a measured coefficient. (Timing grounding — why a feed post + weights recency and a chronicle does not — traces to + `references/algorithm-signals-reference.md`; this file does not restate algorithm + magnitudes.) + +## Mode selection + +| Mode | Use for | Driver | +|------|---------|--------| +| **kortform** | feed posts (`/linkedin:post`, `:quick`, `:react`, `:carousel`, `:video`) | timing + audience pull — a good post published early beats a perfect post published late | +| **long-form** | chronicles / newsletters / series editions (`/linkedin:newsletter`) | depth + angle — a chronicle has a longer shelf life; whether there is *enough material for a full piece* matters more than a 24-hour window | + +If no mode is declared, default to **kortform** (the feed is the higher-cadence surface). +A caller may pass the mode explicitly (e.g. the long-form orchestrator requesting +`long-form`); a future slice may read a per-user default from the profile. + +## Mode: kortform (LinkedIn feed post) + +Tuned for the feed: timing and audience pull carry real weight because the first-mover +window is short. + +| Dimension | Weight | 1–2 (Low) | 3–5 (Medium) | 6–8 (High) | 9–10 (Exceptional) | +|-----------|--------|-----------|---------------|-------------|---------------------| +| **Pillar Fit** | 30 % | Outside all 5 pillars | Tangential to one pillar | Direct hit on one pillar | Intersects 2+ pillars | +| **Audience Relevance** | 25 % | Wrong audience entirely | Some audience overlap | Core audience cares | Audience actively asking about this | +| **Timing** | 20 % | >7 days old, saturated | 3–7 days, moderate coverage | 24–72h, early coverage | <24h, you would be among first | +| **Angle Potential** | 15 % | Only obvious take available | One good angle possible | 2–3 strong angles | Contrarian or unique angle clear | +| **Authority Match** | 10 % | No credibility on topic | Some related experience | Direct experience | Published authority on this | + +``` +Composite = (Pillar × 0.30) + (Audience × 0.25) + (Timing × 0.20) + (Angle × 0.15) + (Authority × 0.10) +``` + +## Mode: long-form (chronicle / newsletter / series) + +Reweighted for a longer piece: **depth potential** enters at 25 %, and **timing drops to +10 %** — a chronicle is not a 24-hour reaction, so recency matters less than whether the +topic has enough substance and a differentiated angle to carry a full edition. + +| Dimension | Weight | 1–2 (Low) | 3–5 (Medium) | 6–8 (High) | 9–10 (Exceptional) | +|-----------|--------|-----------|---------------|-------------|---------------------| +| **Pillar / thesis fit** | 30 % | Outside the active theses | Tangential to one thesis | Fits one thesis / series arc | Advances 2+ theses or a live series arc | +| **Depth potential** | 25 % | A single post at most | Enough for one solid post | Enough for a full chronicle | Enough for a multi-part series | +| **Angle / differentiation** | 20 % | Commodity take only | One non-obvious angle | 2–3 differentiated angles | Original thesis the field lacks | +| **Authority / experience** | 15 % | No credibility on topic | Some related experience | Direct lived experience | Published authority on this | +| **Currency** | 10 % | Stale / already resolved | Relevant this quarter | Relevant now, durable | Relevant now AND will compound | + +``` +Composite = (Pillar × 0.30) + (Depth × 0.25) + (Angle × 0.20) + (Authority × 0.15) + (Currency × 0.10) +``` + +## Why the two modes differ (the one delta) + +The two rubrics are the same five-dimension instrument with one deliberate swap: + +- **kortform:** Timing **20 %**, no depth dimension — the feed rewards being early. +- **long-form:** Timing → Currency at **10 %**, Depth potential added at **25 %** — a + chronicle rewards substance and a durable angle over speed. + +Pillar fit (30 %) and authority (kortform 10 % / long-form 15 %) anchor both: an off-pillar +or low-credibility topic scores low in either mode, because topic relevance and credibility +are non-negotiable regardless of format. + +## Composite → action + +The same priority bands apply to both modes (the composite is on the same 0–10 scale): + +| Composite | Priority | kortform action | long-form action | +|-----------|----------|-----------------|------------------| +| 8.0–10 | **Immediate** | Draft within 24h | Promote to the edition backlog now | +| 6.0–7.9 | **High** | Publish within 48–72h | Strong edition candidate — schedule it | +| 4.0–5.9 | **Medium** | Add to this week's calendar | Hold as a backlog candidate, revisit | +| 2.0–3.9 | **Low** | Note, skip for now | Park unless the angle sharpens | +| 0–1.9 | **Skip** | Off positioning | Off positioning | + +## Consumers + +- `agents/trend-spotter.md` — reads the requested mode and applies the matching rubric + instead of inlining a matrix (wired in research-engine slice 2b). +- Any future research-engine pass that scores candidates before writing them to the trend + store (`scripts/trends/`). diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 3c76fce..6b6ddd7 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -53,12 +53,16 @@ warn() { echo -e "${YELLOW}⚠${NC} $1"; WARN=$((WARN + 1)); } # with the files when adding/removing an agent, command, reference, or skill. EXPECT_AGENTS=19 EXPECT_COMMANDS=29 -EXPECT_REFS=26 +EXPECT_REFS=27 EXPECT_SKILLS=6 -# Pre-M0 references/ baseline was 25; M0 adds exactly one ref doc -# (references/data-path-convention.md). The "delta = +1" assert below proves the -# bump was a single intended addition, not a larger jump masking an extra doc (m3/m11). +# Pre-M0 references/ baseline was 25. Every ref doc added since is NAMED below, so the +# count bump always maps to an intended, named addition — never an incidental doc masked +# by the bump (m3/m11). M0 added data-path-convention.md; each later slice appends its +# doc to POSTM0_REFS. The assert below proves EXPECT_REFS == 25 + 1 (M0) + |POSTM0_REFS| +# AND that every named doc actually exists. bash 3.2-safe: plain indexed array. REFS_BASELINE_PRE_M0=25 +M0_REF="references/data-path-convention.md" +POSTM0_REFS=("references/trend-scoring-modes.md") # research-engine slice 2a (scoring SSOT) echo "================================================" echo "LinkedIn Studio Plugin — Structure Validator" @@ -101,12 +105,19 @@ assert_count "commands/*.md" "$COMMANDS" "$EXPECT_COMMANDS" assert_count "references/*.md" "$REFS" "$EXPECT_REFS" assert_count "skills/*/SKILL.md" "$SKILLS" "$EXPECT_SKILLS" -# M0 references/ delta = exactly +1, and the one added doc is the D3 convention. -# Guards against the bump silently absorbing an incidental extra ref doc (m3/m11). -if [ "$EXPECT_REFS" -eq $((REFS_BASELINE_PRE_M0 + 1)) ] && [ -f references/data-path-convention.md ]; then - pass "references/ delta = exactly +1 (M0 added data-path-convention.md; ${REFS_BASELINE_PRE_M0}→${EXPECT_REFS})" +# references/ count must map 1:1 to the NAMED additions (M0 + every later slice's doc), +# and each named doc must exist. Guards against the bump silently absorbing an incidental +# extra ref doc (m3/m11). To add a ref doc: append it to POSTM0_REFS and bump EXPECT_REFS. +NAMED_REFS_OK=1 +[ -f "$M0_REF" ] || NAMED_REFS_OK=0 +for r in "${POSTM0_REFS[@]}"; do + [ -f "$r" ] || NAMED_REFS_OK=0 +done +EXPECT_NAMED=$((REFS_BASELINE_PRE_M0 + 1 + ${#POSTM0_REFS[@]})) +if [ "$EXPECT_REFS" -eq "$EXPECT_NAMED" ] && [ "$NAMED_REFS_OK" -eq 1 ]; then + pass "references/ count maps to named additions (${REFS_BASELINE_PRE_M0} baseline +1 M0 +${#POSTM0_REFS[@]} post-M0 = ${EXPECT_REFS}; all named docs exist)" else - fail "references/ delta != +1 — M0 must add exactly one ref doc (the D3 convention), not $((EXPECT_REFS - REFS_BASELINE_PRE_M0))" + fail "references/ count != named additions — bump EXPECT_REFS and name the doc in POSTM0_REFS (expected ${EXPECT_NAMED}, have ${EXPECT_REFS}; named-docs-exist=${NAMED_REFS_OK})" fi # Cross-check the CLAUDE.md declared headers against the contract (doc-drift guard)