# sdtk-marketing-kit

**Governed, honest, measured marketing operations — local-first, free, MIT.**

The SDTK governance kernel applied to marketing: this toolkit *enforces* truthfulness (you cannot
ship an overclaim or a faked "demo"), *measures before it targets*, and treats *human approval as a
code gate, not a setting*. It is designed to be **operated by an agent** (Hermes) and **gated by a
human**. Phase 1 ships the two most-proven pieces: a measured KPI **digest** and a blocking
truthfulness **linter**.

## Install

```
npm i -g sdtk-marketing-kit
sdtk-marketing help
```

Requires Node ≥ 18.

## Commands (Phase 1)

### `sdtk-marketing check <file|dir>…`

Lint marketing **post copy** for truthfulness (BR-03 / honest-scorecard). Exits **1** on any error.

- **Blocking errors** — claims that violate the "claims-NOT-made" discipline: `trusted by`,
  unverified adoption/user counts, revenue figures (MRR/ARR), productivity multipliers ("10x
  faster"), zero-touch / unattended / live-bot claims (negated honest usage like *"nothing here is
  zero-touch"* is allowed).
- **Warnings (review, non-blocking)** — framings that are only truthful next to a real asset:
  `unedited`, `real/raw demo`, `screen recording`; and a `stated-count-mismatch` heuristic (catches
  *"Six palettes (a · b · c · d · e · f · g)"* = 6-vs-7).
- Flags: `--json` (structured output for an agent), `--strict` (warnings also block), `--stdin`
  (lint copy piped in — the agent-gate path: pipe the *exact publishable text* so meta docs and asset
  notes don't cause false positives).

```
printf '%s' "$post_body" | sdtk-marketing check --stdin --json   # exit 1 blocks the post
```

**Point it at post copy, not meta docs.** Governance/analysis docs that *discuss* the banned terms
(proposals, audits) will trip the rules by design — lint the actual posts you're about to publish.

```
sdtk-marketing check governance/marketing/SDTK_FB_POST5_*.md --json
```

### `sdtk-marketing digest`

Measured weekly KPI digest: **npm** downloads · **github** stars/forks/issues · **plausible**
visitors · **lemonsqueezy** orders · **facebook** followers.

- npm + github need no keys. The rest read keys from env; **unset keys print `not configured`** — a
  valid state, not a failure (sections light up as you provision keys).
- A configured section that fails prints `FAILED: <reason>` and the run exits **1** (fail-loud).
- **`0` is a real value** (e.g. `lemonsqueezy: orders 7d 0` is a PASS), never hidden.
- Env: `PLAUSIBLE_API_KEY`/`PLAUSIBLE_SITE_ID`, `LEMONSQUEEZY_API_KEY`, `FACEBOOK_PAGE_ACCESS_TOKEN`/`FB_PAGE_ID`; overrides `MKT_NPM_PACKAGES`, `MKT_GITHUB_REPO`. Flag: `--json`.

### `campaign` · `attribution` · `eval` — the eval-first engine (P2.1)

Pre-register a distribution campaign's success criteria, record each channel's measured funnel, and
get **KEEP / ITERATE / TRIM** verdicts. Thresholds **lock** at `campaign start` so a result can't be
rationalised after the numbers land. The funnel is **visitor → pack download → order**.

```
sdtk-marketing campaign start distribution-r2 --goal "find dev/maker channels" \
    --window-hours 72 --keep-visitors 30 --keep-downloads 3 --trim-visitors 10
sdtk-marketing attribution record distribution-r2 --channel devto --visitors 41 --downloads 5 --orders 0
sdtk-marketing eval distribution-r2 [--json]
#   [KEEP   ] devto: 41v → 5d → 0o  (v→d 12.2%)  ↳ ≥30v AND ≥3d — make it a repeatable slot
```

- **Decision:** KEEP = `visitors ≥ keep-visitors AND downloads ≥ keep-downloads`; TRIM = `visitors < trim-visitors`; ITERATE = the middle band, or visitors-ok-but-downloads-short.
- **Ledger:** `.sdtk/marketing/campaigns/<id>/` (`campaign.json` + `attribution.json`) — file-backed, resumable, `--json` for an agent. Override the root with `SDTK_MARKETING_HOME`.
- **Attribution — auto or manual.** `attribution pull <campaign>` auto-fills each channel's
  visitors/downloads from the **Plausible Stats API** per `utm_source` (needs `PLAUSIBLE_API_KEY` +
  `PLAUSIBLE_SITE_ID`; pulls for channels in the `channel` registry that have a `utm`). Or record by
  hand with `attribution record`. Per-channel *orders* stay manual — Lemon Squeezy checkout carries
  no UTM. The value is applying *locked* thresholds to *measured* numbers.

### `channel` — the distribution registry (P2.2)

Tracks each channel's **gate status** and whether it's **owned vs rented**, so an agent proposes a
postable channel instead of hitting a wall (the lesson from every quality channel gating posting —
Reddit karma, IH earned privilege).

```
sdtk-marketing channel add devto --type rented --status open --gate none --utm devto --reach "dev/maker SEO, evergreen"
sdtk-marketing channel add indiehackers --type rented --status warming-up --gate "earned posting privilege"
sdtk-marketing channel status indiehackers ready     # flip when the gate clears
sdtk-marketing channel list                          # splits POSTABLE-NOW vs earn-in
```

- **status:** `open` / `ready` = postable now; `warming-up` / `blocked` / `trimmed` / `paused` = not.
- **type:** `owned` (email/site — no gate, no algorithm) vs `rented` (borrowed reach, plays by a gate).
- Ledger: `.sdtk/marketing/channels.json`. `--json` for an agent.

### `capture` · `asset verify` — the asset truthfulness gate (P2.3)

The visual counterpart to `check`: **an `evidence_capture` must be a real recording of the real
product** — a generation tool (ComfyUI/LTX/WAN/Remotion) can never produce evidence; a
`generated_creative` may only be cosmetic and must declare a truthfulness boundary.

```
# produce provenance-stamped evidence from a LIVE url (pixel grab delegated to your browser tool)
export SDTK_MARKETING_CAPTURE_CMD='node scripts/shot.js {url} {out}'   # operator-configured
sdtk-marketing capture https://sdtk.dev/pricing --out assets/pricing.png --id post5-pricing

# register an AI-generated cosmetic asset (must declare what it may/may not imply)
sdtk-marketing asset add hero-titlecard --role generated_creative --method remotion \
    --truthfulness "cinematic title card — cosmetic only, never implies it is the product"

sdtk-marketing asset verify --all       # exit 1 if any evidence asset is fake / any creative lacks a boundary
```

- **The gate:** `evidence_capture` needs a real capture method (`browser_capture`/`screen_record`) +
  a `source_url`; an AI method in an evidence slot **FAILS**. `generated_creative` needs a
  `truthfulness_boundary`. Exit 1 on any error.
- **`capture` is honest:** it stamps `browser_capture` provenance **only after** a real capture
  actually produced the file (via `SDTK_MARKETING_CAPTURE_CMD`); the kit stays dependency-free and
  never fabricates provenance.
- Ledger: `.sdtk/marketing/assets/<id>.json`.

### `report` — the campaign scorecard (P3)

Composes a campaign's locked thresholds + recorded funnel into a scorecard, and a **measured-only
draft post** in the honest-scorecard voice — the loop feeding itself. The draft is measured by
construction, so it passes `check`:

```
sdtk-marketing report distribution-r2                 # operator scorecard (verdicts + totals)
sdtk-marketing report distribution-r2 --post \
  | sdtk-marketing check --stdin                      # the draft post, self-verified truthful
```

### `video` — reproducible video-build workflows (Phase 3a)

An agent builds channel videos by naming an **exact, reproducible workflow** — the kit holds the
definition + honesty rules, the box does the render. The renderer backends are delegated through operator-owned command templates, mirroring `capture` so the kit stays
dependency-free:

```
sdtk-marketing video list                              # the registry (id · purpose · backend · honesty)
sdtk-marketing video show tutorial-feature-walkthrough # the exact recipe + which delegate env it uses
sdtk-marketing video run tutorial-feature-walkthrough \
  --capture usage.cast --feature usage                 # render → record an honest asset → re-verify
sdtk-marketing video run intro-spectacle --dry-run     # print the exact backend command, render nothing

# Governed terminal evidence: validate first, then composite real argv output with Playwright.
sdtk-marketing video terminal-capture validate --plan terminal-capture-plan.json --run-root "$RUN" --json
sdtk-marketing video terminal-capture run --plan terminal-capture-plan.json --run-root "$RUN" \
  --out "$RUN/artifacts/episode_render/terminal-capture.mp4" --json

# Final narrated explainer: HyperFrames + terminal motion/content/story gate.
sdtk-marketing video run terminal-evidence-explainer --capture "$RUN/artifacts/episode_render/terminal-capture.mp4" \
  --id usage-explainer --slug usage-explainer
```

- **Delegated render:** the box supplies `SDTK_MARKETING_VIDEO_CMD_REMOTION` /
  `SDTK_MARKETING_VIDEO_CMD_COMFYUI` /
  `SDTK_MARKETING_VIDEO_CMD_HYPERFRAMES` (templates with `{graph} {capture} {input} {out}`). Unset →
  fail-closed, nothing stamped.
- **Output stays outside the kit:** MP4 lands under `SDTK_MARKETING_ASSET_HOME` (the ledger keeps
  definitions + provenance, not bytes).
- **Honesty is enforced twice (D3):** a `tutorial`/`review` is `evidence_capture` and **requires a
  real `--capture`** (a screen recording of the actual CLI) — the kit will not
  fabricate a tutorial from a generator; ComfyUI/LTX/WAN output is `generated_creative`, allowed for
  intro/b-roll only and must carry a truthfulness boundary. The produced asset is then re-checked by
  the same `asset verify` gate.
- **Governed terminal capture:** `video terminal-capture` binds one run-local DEMO fixture, evidence
  manifest SHA, exact argv commands, browser boundary, capture thresholds, final timeline coverage, and a visual contract: native terminal window, titles in the header bar only, and no decorative horizontal motion over proof.
  The box supplies `SDTK_MARKETING_VIDEO_CAPTURE_CMD_PLAYWRIGHT_TERMINAL` with
  `{plan} {run_root} {out}`. Missing delegates, stale manifests, unsafe paths, unsupported commands,
  low-quality captures, or browser-contract drift fail closed and produce no receipt.
- **Quality gate:** honesty isn't enough — a tiny/short stub render passes every honesty check and
  still wastes an upload. Each workflow declares minimum specs; the runner probes the real output via
  `SDTK_MARKETING_VIDEO_PROBE_CMD` (a `{file}` template) and a sub-spec render **fails closed**
  instead of being recorded. If no probe is configured, the gate is skipped with a warning.

  Size and duration alone were **not** enough, twice: a 1920×1080 / 300 s render passed and turned out
  to be a slideshow (frozen 97.7 % of its runtime), and the motion gate added to fix that then passed
  a *moving empty screen* (27 % near-blank frames). So the probe may also report **motion and
  content**, and a workflow may gate on them:

  | Probe field | Workflow threshold | Catches |
  |---|---|---|
  | `median_mafd` | `min_median_mafd` | a render that barely moves |
  | `frozen_ratio` | `max_frozen_ratio` | a slideshow in motion-heavy profiles |
  | `low_motion_ratio` | `max_low_motion_ratio` | too much low motion in narrated explainers (MAFD < 0.10) |
  | `low_motion_run_s` | `max_low_motion_run_s` | one uninterrupted static-feeling stretch |
  | `edge_density` | `min_edge_density` | an empty screen |
  | `luma` | `luma_range` | too dark to read, or blown out |

  The probe accepts either the original `"<w> <h> <duration>"` or `key=value` pairs:

  ```
  width=1920 height=1080 duration=78 median_mafd=0.127 frozen_ratio=0.982 low_motion_ratio=0.246 low_motion_run_s=1.633 edge_density=5.33 luma=42.1
  ```

  Existing probe commands keep working untouched — a threshold whose field the probe does not report
  is announced as **NOT ENFORCED** rather than silently passing. Final audit records may additionally
  carry `visual_quality` evidence: maximum composition hold, meaningful visual-state changes, product
  proof start time, and narration-to-visual-action coverage. When declared in the creative plan, all
  four are required and fail closed.

- **`video calibrate` — derive your own thresholds.** The shipped numbers were calibrated on
  light-mode product UI plus one dark-composite film, and each workflow carries a
  `calibrated_against` note saying so. They are not universal: a threshold picked by intuition once
  rejected a capture that measured *better* than the reference film's own signature frame. Point this
  at footage you consider good and paste the result into your workflow:

  ```
  sdtk-marketing video calibrate ref-a.mp4 ref-b.mp4          # measured spread + suggested fields
  sdtk-marketing video calibrate ref-a.mp4 --margin 0.2 --json
  ```

  **None of these gates certify that a video is good.** They rule out defects that were actually hit.
  Watch it before you publish it.

### Evidence-Bound Production Profile

Use the `feature-proof-episode` profile before a final render when a feature video must be judged on visible product progression rather than decorative motion. It binds duration to meaningful product actions, limits repeated visual families, maps narration to visible action, and requires SHA-bound derived evidence before a final audit can pass.

```bash
sdtk-marketing video project production <project-id> record --file production-plan.json
sdtk-marketing video project production <project-id> check --phase pre-render --json
sdtk-marketing video project production <project-id> evidence --file derived-evidence.json --render-sha <final-mp4-sha256>
```

This profile is a pre-render quality contract, not an automatic aesthetic certificate. Owner picture lock remains required.

### `video project` — renderer-neutral production evidence (Phase B)

The existing `video run` recipes remain the lightweight channel-video surface. Use `video project` when a production needs a brief, storyboard, asset/claim/transition ledgers, review evidence, and explicit owner decisions that must survive a handoff.

```bash
sdtk-marketing video project init refund-approval-film
sdtk-marketing video brief validate brief.json --json
sdtk-marketing video storyboard validate storyboard.json --json
sdtk-marketing video asset add refund-approval-film --file asset.json
sdtk-marketing video asset verify refund-approval-film --json
sdtk-marketing video project diagnose refund-approval-film --file storyboard.json --json
sdtk-marketing video review refund-approval-film --file review.json
```

- Project JSON is file-backed under `.sdtk/marketing/video-projects/<id>/`; it stores paths and SHA-256 references, never heavy media or secrets.
- `playwright_screen_record` normalizes to `screen_record` so a real browser capture does not fail due to a method spelling difference. Unknown methods still fail closed.
- Diagnostics are advisory. They expose timing/declaration risks but cannot create a creative pass. A review record stores an explicit owner decision and **never** advances project state automatically.
- HyperFrames is an operator-configured delegate through `SDTK_MARKETING_VIDEO_CMD_HYPERFRAMES`. `video project render … --dry-run` prints the exact expansion; missing artifacts/delegate/output fail closed. Rendering does not publish or stamp a valid asset by itself. Use `video preview <project> --mode timeline --port <port> --tunnel` only for an explicitly owner-authorized temporary Cloudflare Quick Tunnel; it is receipt-bound and stopped with preview.

See [the social-copy workflow contract](docs/SOCIAL_COPY_WORKFLOW.md). Social `publish-prepare` now binds selected YouTube/Facebook copy to the exact `FILM_ACCEPTED` bytes before it can enter the existing sha-gated upload path; X remains copy-only.

See [the high-quality local video workflow](docs/HIGH_QUALITY_VIDEO_WORKFLOW.md), [the project workflow contract](docs/VIDEO_PROJECT_WORKFLOW.md), and [the second-project dogfood record](docs/VIDEO_PROJECT_SECOND_DOGFOOD.md).

See [the governed terminal capture contract](docs/TERMINAL_CAPTURE_WORKFLOW.md) for the reusable real-command Playwright boundary.

### `publish youtube` / `publish facebook` — attended, sha-gated video publish (Phase 3b/3c)

A video reaches YouTube (or a Facebook Page) **only after the owner approves this exact payload**. The
upload is impossible without a matching approval sha, so an agent cannot auto-post even if told to:

```
# 1) prepare — verifies the asset, guarantees the UTM, check-gates the copy, prints a payload sha
sdtk-marketing publish youtube tutorial-usage \
  --title "How I run sdtk usage in 60s" --description-file desc.md --tags "sdtk,cli,devtools"
#    → prints the drafted metadata + `payload sha256: <hex>` and uploads NOTHING

# 2) approve — re-run the identical command with the sha; only a MATCH uploads
sdtk-marketing publish youtube tutorial-usage \
  --title "How I run sdtk usage in 60s" --description-file desc.md --tags "sdtk,cli,devtools" \
  --approve <hex>
```

- **The asset gate first:** publish refuses an asset that fails `asset verify` (no faked evidence
  ships) and refuses if the MP4 is missing on disk.
- **The copy gate:** the title + description run through `check` — an overclaim blocks the publish.
- **Attribution built in:** the description is guaranteed to carry
  `sdtk.dev/heroes?utm_source=youtube&utm_medium=video&utm_campaign=<id>`, and a successful publish
  upserts the `youtube` channel — so `attribution pull` measures it automatically.
- **Delegated upload, no secrets in the kit:** the actual upload runs via the box's delegate
  (`SDTK_MARKETING_PUBLISH_CMD_YOUTUBE` / `SDTK_MARKETING_PUBLISH_CMD_FACEBOOK_VIDEO`, a
  `{file} {title} {description-file} {tags} {privacy}` template); the OAuth/Page token lives on the
  box. Unset → fail-closed, nothing uploaded. The delegate must print a canonical `https://…` URL; relative or non-HTTPS output is rejected without a record.
- **Conservative by default:** `--privacy` defaults to the safest state per platform (youtube
  `unlisted`, facebook `unpublished`) — the sha-gate approves the upload; going fully public stays a
  separate deliberate flip on the platform. Non-public uploads are recorded as `uploaded` with their actual visibility state, never reported as public posts.
- **Drift = fail-closed:** change any metadata after approval and the sha no longer matches — the
  upload is refused until the new payload is re-approved. The channel and payload are part of the
  hash, so a YouTube approval can never be replayed against Facebook.

## What's coming (see the toolkit proposal)

An MCP / n8n surface exposing the whole governed spine (check · eval · video · publish) to an agent
runtime. The attended `schedule` / `gate approve` / `publish` loop is provided by HerSocial today;
this kit standardizes it.

## Design principles

- Never auto-publish; never fabricate a metric or a claim; AI only for cosmetic assets.
- Not a spam / mass-posting tool — respects channel gates, eval-first.
- File-backed and resumable; every command emits `--json` for an agent operator.

## Develop

```
npm test   # node scripts/marketing-smoke.test.js — offline, no network
```

License: MIT.
