# Governed Social Copy Workflow

`video social` turns an owner-accepted video project into reviewed platform copy. It does not render, publish, call an upload adapter, or hold an LLM provider credential.

## Invariants

- The project requires an explicit owner review of `FILM_ACCEPTED`; recording that decision does not silently advance project state.
- R1 accepts English video briefs and produces `en` candidates only.
- `qualified_visit` is the only campaign KPI in R1. Each prepared payload has its own `utm_content=<variant-id>`.
- Candidate claim references must point only to `grounded` claims in the video claim ledger. `blocked` and `demo_data` claims cannot silently become public copy.
- Candidate schema v2 requires a declared `hook`, exact grounded `proof`, `outcome`, and source-bound `cta`; the final platform payload must contain them in its prescribed order. Existing v1 candidate files must be regenerated.
- At most 15 candidates are accepted. The normal matrix is three angles (`pain`, `proof`, `workflow`) across `x`, `facebook`, and `youtube`.
- `prepare` is not publish and emits no approval command. It produces platform-specific copy records; `publish-prepare` later binds a selected YouTube/Facebook record to exact accepted-film bytes and produces the only upload SHA.
- X is copy-only in R1. A missing X adapter is a deliberate fail-closed boundary, not a fallback to an unaudited upload.

## Commands

```bash
sdtk-marketing video social source refund-approval-film \
  --campaign distribution-r2 --goal qualified_visit \
  --landing-url https://sdtk.dev/heroes \
  --platforms x,facebook,youtube --angles pain,proof,workflow

export SDTK_MARKETING_SOCIAL_COPY_CMD='node /opt/marketing/copy-delegate.js --input {source} --output {out}'
export SDTK_MARKETING_SOCIAL_COPY_TIMEOUT_MS=60000
sdtk-marketing video social generate refund-approval-film
sdtk-marketing video social validate refund-approval-film --json
sdtk-marketing video social list refund-approval-film
sdtk-marketing video social select refund-approval-film --platform x --variant x-proof-01
sdtk-marketing video social prepare refund-approval-film --json

# Binds the selected platform copy to exactly the owner-accepted film bytes. No upload.
sdtk-marketing video social publish-prepare refund-approval-film \
  --platform youtube --file /media/refund-approval-final.mp4 --json

# Only the matching SHA runs the existing attended publisher delegate.
sdtk-marketing video social publish refund-approval-film \
  --platform youtube --approve <publisher-payload-sha256> --json
```

## Delegate Contract

The delegate receives a source JSON path and writes one JSON object to its output path. It must not invent claim IDs, change the project ID or source identity, or return more than 15 variants.

```json
{
  "schema_version": "sdtk.marketing-social-candidates.v2",
  "project_id": "refund-approval-film",
  "source_identity_sha256": "<source digest>",
  "language": "en",
  "canonical_concept": "One grounded idea reused across platforms.",
  "variants": [{
    "quality": {
      "hook": "A concise problem-first opening.",
      "proof": { "claim_ref": "C01", "text": "Exact grounded claim text." },
      "outcome": "The concrete user outcome.",
      "cta": "Exact CTA from the source pack.",
      "search_terms": ["required for youtube only"]
    }
  }]
}
```

Platform contracts are strict:

- X: `format` is `single` or `thread`; the first post starts with `hook`, all quality fields appear in the assembled copy, and every post passes `twitter-text` weighted length validation.
- Facebook: `format` is `video_post`; `page_copy` starts with `hook`, and `first_comment` is required to contain the declared CTA.
- YouTube: `format` is `video`; `description` starts with `hook` and contains proof/outcome/CTA; its title contains a declared search term; it has 3–8 unique tags and a pinned discussion question.

Every variant references one or more grounded source claims. The proof text must exactly match its referenced grounded claim; the CTA must match the source pack. The kit checks exact assembled copy before it records candidates and again after it adds tracking parameters during prepare.

## Ledger And Failure Behavior

```text
$SDTK_MARKETING_HOME/video-projects/<project-id>/social/
  source.json
  candidates.json
  selection.json
  prepared-x.json
  prepared-facebook.json
  prepared-youtube.json
  publisher-facebook.json
  publisher-youtube.json
```

Social records use atomic rename and contain no token or provider credential. A publisher record references the accepted film SHA and adds a generic asset projection for the existing publisher; it does not duplicate video bytes or become a second film source of truth. Missing source/delegate, timeout, malformed JSON, ungrounded claims, check failures, source-selection drift, copy drift, file-byte drift, or a wrong approval SHA block the step. No automatic retry or platform mutation occurs.

## Publisher Bridge

`publish-prepare` supports only `youtube` and `facebook`. It verifies that `--file` hashes to the `FILM_ACCEPTED` asset, then revalidates the current final audit, visual review, owner decision, and any motion-only quality exception against that exact render. Missing, stale, or mismatched quality evidence blocks before any publisher record or generic asset projection is written. It then runs the exact title/description through `check`, binds the existing publisher payload hash, and emits a grammar-safe `APPROVE HERSOCIAL POST <post_key> <sha256>` command. It uploads nothing.

`publish` repeats the same quality eligibility check alongside source identity, selected variant, prepared-copy SHA, film bytes, and publisher payload before calling the existing `publish youtube|facebook` delegate. The owner approval is therefore bound to one platform, one accepted film, one reviewed visual state, and one exact metadata payload.

The present uploader adapters publish video bytes plus title/description/tags only. A Facebook `first_comment` and a YouTube `pinned_comment` are stored as `manual_follow_up_required`; the toolkit never reports them as posted. Delegates must emit one canonical `https://…` permalink. A relative, `http://`, missing, or malformed URL is fail-closed and writes no publish record. An `unpublished` Facebook upload is recorded as `uploaded` with `visibility_state: unpublished`, not as a public publish; the owner must make it public in Facebook after review.
