From 1a67bd2cb85eb5fecaff39d0e8775d3dd674d98b Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 16 Jul 2026 20:47:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(linkedin-studio):=20N3=20=E2=80=94=20Step?= =?UTF-8?q?=207.5=20kodet-figur-rute=20+=20figure-design-guidelines=20[ski?= =?UTF-8?q?p-docs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MR-F4 S2: (1) commands/newsletter.md Step 7.5 — generate er nå tre ruter; kodet rute (render/build-figur.mjs) er PRIMÆR for data-figurer (presisjon/reproduserbarhet), mcp-image beholdes for illustrative, external uendret. Fasetabell + ressursliste + rute-statuslinje oppdatert. (2) NY references/figure-design-guidelines.md — designregler (24pt, fargebudsjett, whitespace), token-konvensjon (profile/brand-tokens.json, --figur-*, nøytrale defaults), tre render-mål. Refs 27->28. (3) CLAUDE.md Architecture-linje for rendereren. scripts/test-runner.sh: EXPECT_REFS 27->28 + fila navngitt i POSTM0_REFS (named-additions-vakta). Suiter grønne på floor: 138/0 - 140/0 - 35/0 - 53/0 - 245/0 - 134/0. Co-Authored-By: Claude Fable 5 Claude-Session: e70c619c-419b-4229-acaa-b5291f5e75d6 --- CLAUDE.md | 1 + commands/newsletter.md | 32 ++++++++---- references/figure-design-guidelines.md | 72 ++++++++++++++++++++++++++ scripts/test-runner.sh | 7 ++- 4 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 references/figure-design-guidelines.md diff --git a/CLAUDE.md b/CLAUDE.md index 5777c27..6fd6dfe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,7 @@ Full-spectrum LinkedIn content engine — short-form feed posts, carousels, vide - **State updater:** `hooks/scripts/state-updater.mjs` — deterministic state mutations (post tracking, streak, content history pruning). Pure functions, tested, no AI involvement - **Clipboard helper:** `hooks/scripts/clipboard-helper.mjs` — cross-platform clipboard (macOS `pbcopy`, Linux `xclip`/`xsel`, WSL `clip.exe`). All content commands auto-copy - **iCal generator:** `hooks/scripts/ical-generator.mjs` — RFC 5545 calendar files for batch scheduling. Standalone CLI + importable module +- **Figure renderer:** `render/build-figur.mjs` — coded data figures (SVG/HTML → PNG via headless Chrome); three targets (article/carousel/single); brand tokens from the user data dir's `profile/brand-tokens.json`, neutral defaults otherwise (see `references/figure-design-guidelines.md`). Standalone CLI + importable module - **Post queue:** `assets/drafts/queue.json` (managed by `hooks/scripts/queue-manager.mjs`) - **Analytics:** CLI `scripts/analytics/` (TypeScript, needs `tsx` + `npm install`); data `assets/analytics/` (gitignored) - **Analytics metrics (S16):** parsed CSV columns + an optional, manually-entered `saves` count (count-only in native LinkedIn analytics since ~Sept 2025, no CSV export / no API). `parseOptionalCount()`: blank / non-numeric / negative → `undefined` (`unknown`, never 0), a genuine `0` is kept; saves surfaced per-post + as `totalSaves`, but **not** folded into `engagementRate`. `dwell` stays **explicitly unmeasurable** (internal to LinkedIn, no export/API). All analytics I/O routes through the `getAnalyticsRoot()` seam (M0 per-user data-dir). diff --git a/commands/newsletter.md b/commands/newsletter.md index f4f837c..a43187c 100644 --- a/commands/newsletter.md +++ b/commands/newsletter.md @@ -109,7 +109,7 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias). | 6 | **Persona sweep — BEFORE lock** | reader jury, primær wins, convergence to clean YES | **`persona-reviewer`** (resonance mode) | | 6.5 | **Headless adversarial review — BEFORE lock** | COLD review package on a frozen draft, no drafting-session context: content-reviewer (argument) + language-reviewer (Norwegian) + fact-reviewer (cold re-verification incl. pivot premises) + persona-reviewer resonance/conversion. Consolidated, operator-gated via `SendUserFile`. The independence layer the in-session gates can't be. | **`content-reviewer` + `language-reviewer` + `fact-reviewer` + `persona-reviewer`** (parallel) + `SendUserFile` | | 7 | **Annotation (optional)** | render annotatable review HTML for a manual pass | `render/build-html.mjs` | -| 7.5 | **Visual assets — BEFORE lock** | cover (+ optional inline figures) or carousel deck: behov → per-image brief → generate (mcp-image default / external `cover-raw.png`) → operator-gate (`SendUserFile`) → approve to `cover.png` → credit/caption. Runs before lock so the renderer picks the cover up. | `mcp__mcp-image__generate_image` + `SendUserFile` + (carousel) `render/build-carousel.mjs` | +| 7.5 | **Visual assets — BEFORE lock** | cover (+ optional inline figures) or carousel deck: behov → per-image brief → generate (coded `build-figur.mjs` primary for data figures / mcp-image for illustrative / external `cover-raw.png`) → operator-gate (`SendUserFile`) → approve to `cover.png` → credit/caption. Runs before lock so the renderer picks the cover up. | `render/build-figur.mjs` (data figures) + `mcp__mcp-image__generate_image` + `SendUserFile` + (carousel) `render/build-carousel.mjs` | | 8 | **LOCK → delivery** | POST.html "all in one place" | `render/build-linkedin.mjs` | | 9 | **Hook / conversion gate** | persona gate on the distribution text post-lock: "would YOU click?" | **`persona-reviewer`** (conversion mode) | | 10 | **Scheduling** | register the edition in the plugin queue/state for native scheduling | `hooks/scripts/queue-manager.mjs` | @@ -1466,13 +1466,25 @@ operator declares carousel format for it. Branch accordingly: in `edition-state.json` → `articles.NN.visualAssets.cover.brief` and `…figures[].brief`. -3. **Generate — two routes, no lock-in.** The interface is pluggable (path-in / - path-out); `mcp-image` is the default, not a hard dependency: - - **Default route — `mcp__mcp-image__generate_image`** (Nano Banana Pro / - Gemini 3 Pro Image). Write candidates to - `linkedin/NN/cover-v-kandidat.png` (and `fig-kandidat.png` for - figures). Candidate naming lets several attempts sit side by side without - overwriting an approved file. Record route `"mcp-image"`. +3. **Generate — three routes, no lock-in.** The interface is pluggable (path-in / + path-out). Route by what the image *is*: + - **Coded route — PRIMARY for data figures** (charts, diagrams, before/after + comparisons — anything whose content is real numbers or real structure). + Author the figure as SVG/HTML per + `${CLAUDE_PLUGIN_ROOT}/references/figure-design-guidelines.md` (design + rules + brand-token convention), then render to a candidate: + ```bash + node "${CLAUDE_PLUGIN_ROOT}/render/build-figur.mjs" linkedin/NN/fig.svg --target article --out linkedin/NN/fig-kandidat.png + ``` + Precision and reproducibility beat generative output for data — numbers, + labels, and proportions are exact, and the figure re-renders identically + after a correction. Record route `"coded"`. + - **Generative route — `mcp__mcp-image__generate_image`** (Nano Banana Pro / + Gemini 3 Pro Image) for **illustrative** images (cover art, mood, + metaphor). Write candidates to `linkedin/NN/cover-v-kandidat.png` (and + `fig-kandidat.png` for figures). Candidate naming lets several attempts + sit side by side without overwriting an approved file. Record route + `"mcp-image"`. - **External route** — DALL·E, Midjourney, a photographer, a hand-built SVG. The plugin accepts a `linkedin/NN/cover-raw.png` the operator drops in; no tool is mandated. Record route `"external"`. (The raw file may then be @@ -1560,7 +1572,7 @@ Visual assets (BEFORE lock). - Cover: linkedin/NN/cover.png approved (after candidates) (or: N/A — carousel) - Figures: approved → linkedin/NN/figN.png (or: none) - Carousel deck: linkedin/NN/carousel.pdf rendered + approved (or: N/A — standard) -- Route: mcp-image | external Credit/caption: recorded in image-credit-caption.md + edition-config.json +- Route: coded | mcp-image | external Credit/caption: recorded in image-credit-caption.md + edition-config.json - Operator gate: approved (candidates surfaced via SendUserFile) [OPERATØR] Next: Step 8 — LOCK → delivery. ``` @@ -1841,3 +1853,5 @@ the honest decision surface; it sells nothing. - `${CLAUDE_PLUGIN_ROOT}/render/build-linkedin.mjs` — POST.html delivery; reads `linkedin/NN/cover.png` + credit/caption (Step 8) - `${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs` — annotatable review renderer (Step 7) - `${CLAUDE_PLUGIN_ROOT}/render/build-carousel.mjs` — carousel deck renderer (`## SLIDE N —` → PDF via weasyprint) — Step 7.5 carousel branch +- `${CLAUDE_PLUGIN_ROOT}/render/build-figur.mjs` — coded data-figure renderer (SVG/HTML → PNG via headless Chrome; targets article/carousel/single) — Step 7.5 coded route +- `${CLAUDE_PLUGIN_ROOT}/references/figure-design-guidelines.md` — coded-figure design rules + brand-token convention + render targets — Step 7.5 diff --git a/references/figure-design-guidelines.md b/references/figure-design-guidelines.md new file mode 100644 index 0000000..6118264 --- /dev/null +++ b/references/figure-design-guidelines.md @@ -0,0 +1,72 @@ +# Figure Design Guidelines — coded figures + +Design rules and conventions for **coded figures**: SVG/HTML sources rendered to +PNG with `${CLAUDE_PLUGIN_ROOT}/render/build-figur.mjs`. Coded is the **primary +route for data figures** (charts, diagrams, comparisons — anything whose content +is real numbers or real structure): precision and reproducibility beat generative +output, and the figure re-renders identically after a correction. Generative +images (`mcp-image`) remain the route for **illustrative** work (cover art, mood, +metaphor). Strategy-level guidance on *when* a post needs a visual at all lives in +`linkedin-visual-style.md`. + +## The three render targets + +| Target | Size | Use | +|--------|------|-----| +| `article` (default) | 1200 × content-driven height (aspect derived from the source's `viewBox`/dimensions; fallback 16:9) | inline figures in long-form articles | +| `carousel` | 1080 × 1350 (4:5) | carousel/document-post slides | +| `single` | 1200 × 1200 (1:1) | standalone feed-post image | + +`--width N` / `--height N` override any target. If an `article`-target source +declares no dimensions, the renderer warns and falls back to 16:9 — set an +explicit `viewBox` (SVG) or `--height`. + +```bash +node "${CLAUDE_PLUGIN_ROOT}/render/build-figur.mjs" figN.svg --target article --out figN.png +``` + +CLI and importable module (`renderFigure` is async). No npm dependencies; needs +headless Chrome (auto-discovered: app bundle → PATH → error with install hint). +No network access during render — sources must be local and self-contained. + +## Design rules + +Enforced as **warn-only validation** by the renderer (never a hard fail); treat +warnings as a review checklist, not noise: + +1. **Text ≥ 24pt/px effective size.** Figures are read on a phone in the feed — + smaller text is illegible at feed scale. (Same floor as the carousel rules in + `linkedin-visual-style.md`.) +2. **Color budget: max 1 primary + 1 secondary beyond neutrals.** Grays, white, + and black are free; every additional hue must earn its place. More colors read + as noise, not information. +3. **≥ 40 % whitespace — an editorial rule, not machine-checked.** Density is the + most common coded-figure failure. If the figure needs a legend to be parsed, + it probably needs to be two figures. + +## Brand-token convention + +The renderer injects design tokens as CSS variables (`--figur-*`) so figure +sources stay brand-agnostic. Tokens are **user data**, read from the data root +(see `data-path-convention.md`): + +``` +${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/brand-tokens.json +``` + +| Key | CSS variable | Neutral default | +|-----|--------------|-----------------| +| `background` | `--figur-background` | `#FFFFFF` | +| `ink` | `--figur-ink` | `#1A1A1A` | +| `muted` | `--figur-muted` | `#5B5B5B` | +| `accent` | `--figur-accent` | `#2F6F9F` | +| `rule` | `--figur-rule` | `#D9D9D9` | +| `fontFamily` | `--figur-fontFamily` | `system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif` | + +Missing or unparsable file → neutral defaults with a warning. Partial files are +merged over the defaults. The user's brand is **data, never hardcoded** — figure +sources reference `var(--figur-accent)` etc. and render correctly for any user. + +**Authoring rule:** use the `--figur-*` variables for every color and font in a +figure source. A hex value hardcoded in the source defeats the token seam and +counts against the color budget. diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index febef8b..7de38c3 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -92,7 +92,7 @@ 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=27 +EXPECT_REFS=28 EXPECT_SKILLS=6 # 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 @@ -101,7 +101,10 @@ EXPECT_SKILLS=6 # 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) +POSTM0_REFS=( + "references/trend-scoring-modes.md" # research-engine slice 2a (scoring SSOT) + "references/figure-design-guidelines.md" # MR-F4 N3 (coded-figure design rules + token convention) +) echo "================================================" echo "LinkedIn Studio Plugin — Structure Validator"