feat(linkedin): v2.3.0 — Step 7.5 visual-assets phase in /linkedin:newsletter

Endring 7 from the change-spec: make visual assets an explicit pipeline
phase. New Step 7.5 (visual-assets) between annotation (Step 7) and lock
(Step 8): cover (+ optional inline figures) or carousel deck, generated and
operator-gated BEFORE lock so build-linkedin.mjs picks up cover.png at lock
without a post-lock re-render. Pipeline 13 → 14 phases.

- commands/newsletter.md: Step 7.5 section, pipeline overview + build-status,
  resumption table (annotation → 7.5; new visual-assets → 8), Step 8
  precondition, reference-file list.
- config/edition-state.template.json: visual-assets phase + additive
  articles.NN.visualAssets schema (format / cover / figures / carousel).
- config/image-credit-caption.template.md (new): motif + credit + caption
  table, honest-about-AI credit, naming convention.
- Two generation routes, no lock-in: default mcp-image (cover-v<N>-kandidat.png)
  or external cover-raw.png. Operator-gate via SendUserFile → cp to cover.png.
  Carousel branch reuses render/build-carousel.mjs.
- Doc/orchestration-only — no new code. Commands (24) + agents (15) unchanged.
- Version sync 2.2.0 → 2.3.0 across plugin.json, CHANGELOG, README, CLAUDE.md,
  root README + root CLAUDE.md.

Correction: spec claimed build-linkedin.mjs handles fig1-4; verified it does
NOT — it embeds only cover.png by fixed name; figures are referenced in the
draft markdown and uploaded manually. Step 7.5 documents actual behavior.

All 8 acceptance criteria met. JSON valid (14 phases); 20/20 render tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-28 22:49:19 +02:00
commit 7ebd28cb0b
9 changed files with 304 additions and 29 deletions

View file

@ -15,11 +15,13 @@
"factcheck-sweep — risk-sorted, guilty-until-disproven, verification log (Step 5)",
"persona-sweep-prelock — reader jury, primary wins, convergence to clean YES (Step 6)",
"annotation — optional annotatable review HTML for a manual pass (Step 7)",
"visual-assets — cover (+ optional inline figures) or carousel deck: brief → generate → operator-gate → approve, BEFORE lock so build-linkedin.mjs picks them up (Step 7.5)",
"lock-delivery — LOCK → POST.html all-in-one-place deliverable (Step 8)",
"hook-conversion-gate — persona gate on distribution text post-lock: would YOU click? (Step 9)",
"scheduling — register edition in plugin queue/state for native LinkedIn scheduling (Step 10)"
],
"articleStatusValues": ["pending", "in-progress", "locked", "scheduled"]
"articleStatusValues": ["pending", "in-progress", "locked", "scheduled"],
"visualAssets": "Per-article visual-asset record written by Step 7.5 (visual-assets phase). Runs BEFORE lock because render/build-linkedin.mjs picks up linkedin/NN/cover.png + the edition-config credit/caption when it builds POST.html — generating images after lock would force a re-render. Shape: { format: \"standard\" | \"carousel\"; cover: { brief, route, candidates[], approved, status }; figures: [ { id, brief, placement, status } ]; carousel: null | { source, pdf, status } }. format \"standard\" = cover + optional inline figures (cover.png is mandatory per the KTG cover-directive); format \"carousel\" = typografisk deck via render/build-carousel.mjs instead of cover+inline (cover/figures stay empty). route: \"mcp-image\" (default, via mcp__mcp-image__generate_image) | \"external\" (DALL·E / Midjourney / photographer → linkedin/NN/cover-raw.png). status ladder: pending → briefed → generated → approved. candidates[] holds the cover-v<N>-kandidat.png attempts; approved is the fixed approved name (\"cover.png\") once the operator-gate passes. figures[].id = \"fig1\"..; placement = section reference in NN-utkast.md (figures are referenced in the draft via ![alt](linkedin/NN/figN.png) and uploaded manually in the LinkedIn editor — build-linkedin.mjs does NOT embed them). Naming convention: cover.png (approved, fixed — what build-linkedin.mjs reads) | cover-v<N>-kandidat.png (attempts) | cover-raw.png (optional external pre-edit source) | fig<N>.png (inline). credit + caption are recorded in <serie>/linkedin/image-credit-caption.md and flow into edition-config.json coverCredit + captions[NN]."
},
"schemaVersion": 1,
"series": {
@ -41,6 +43,18 @@
"resonance": null,
"conversion": null
},
"visualAssets": {
"format": "standard",
"cover": {
"brief": null,
"route": null,
"candidates": [],
"approved": null,
"status": "pending"
},
"figures": [],
"carousel": null
},
"locked": false,
"scheduled": null
}

View file

@ -0,0 +1,63 @@
# Bilde-credit + caption — cover per edition
> **TEMPLATE.** Copy this to `<serie>/linkedin/image-credit-caption.md` and fill it
> in per series. `/linkedin:newsletter` Step 7.5 (visual-assets phase) reads it and
> updates the row for the edition in production; the values flow into
> `<serie>/linkedin/edition-config.json``coverCredit` + `captions[NN]`, which
> `render/build-linkedin.mjs` reads when it builds `POST.html` (Step 8). This file
> is the human-readable source of truth for *motif + credit + caption*; the JSON is
> the machine copy the renderer consumes.
LinkedIn-editoren har et **«Add credit and caption»**-felt under hvert bilde. Fyll
inn per cover. Caption = én kort linje som koder artikkelens signal (det leseren
skal sitte igjen med), ikke en bildebeskrivelse.
> Format i editoren: ofte ett felt. Lim «Caption — Credit» eller bruk feltene hver
> for seg om de finnes.
## Verifiseringsplikt — credit skal være ærlig
Er coveret **KI-generert** (Nano Banana Pro / Gemini / DALL·E / Midjourney) →
credit MÅ si det. Aldri la et AI-bilde framstå som foto eller egenprodusert
illustrasjon. Eksempel-credit for AI-cover:
**Felles credit (alle editions):** `Illustrasjon generert med <verktøy>` — f.eks.
`Illustrasjon generert med Google Gemini (Nano Banana Pro)`.
Er coveret et ekte foto eller en håndlaget figur → bytt til den ærlige creditten
(`Foto: <fotograf>`, `Egenprodusert figur`). Avvik fra felles-creditten føres under.
**Per-edition credit-avvik:** _(list any edition whose credit differs from the
felles-credit, with the reason — e.g. «Del 3: Egenprodusert figur (kodet SVG)».
None by default.)_
## Motiv + caption per edition
| Del | Cover (motiv) | Caption |
|-----|---------------|---------|
| 01 | _<one-line motif — what the cover depicts>_ | _<one-line caption — the article's signal>_ |
| 02 | __ | __ |
| samle | _<optional samle-post badge/motif>_ | _<optional>_ |
## Naming-konvensjon (cover-filer)
- `cover.png`**godkjent, fast navn**. Det eneste filnavnet `build-linkedin.mjs`
leser. Operator-gaten i Step 7.5 kopierer den godkjente kandidaten hit.
- `cover-v<N>-kandidat.png` — genererings-forsøk (mcp-image eller etterbehandlet).
Flere kan ligge side om side uten å overskrive den godkjente.
- `cover-raw.png` — valgfri ekstern pre-edit-kilde (DALL·E / Midjourney / fotograf).
- `fig<N>.png` — inline-figur (`fig1.png`, `fig2.png`, …), referert fra utkast-markdown
med `![alt](linkedin/NN/figN.png)` og **lastet opp manuelt** i editoren
(`build-linkedin.mjs` embedder ikke figurer).
## Carousel-utgaver
Carousel-editions (typografisk deck via `render/build-carousel.mjs`) har som regel
**ingen foto-cover** → ingen bilde-credit nødvendig. Slide-kilden er
`linkedin/NN/carousel.md`, rendret til `linkedin/NN/carousel.pdf`. En carousel-edition
som *også* legger en feed-cover trenger likevel en rad over.
## Samle-post
Ev. Maskinrommet-/serie-badge (egen asset) → ingen credit. Lenken til serien ligger i
første kommentar, ikke i bildet.