Campaign preview (offline, per channel)
reachweaver campaign-preview shows how each variant of a campaign looks on its channel before anyone stages or approves it. reachweaver/preview.py is deterministic: the same campaign, brand, accounts and registry always give the same preview. It makes no network calls, needs no credentials or database (an optional --history-db is only read), writes nothing, and never modifies the campaign file. No copy is generated. Every string in the preview comes from the campaign file, and no language model or paid API is involved.
reachweaver campaign-preview --file examples/campaign-preview.reachweaver.json --brand examples/brand-preview.reachweaver.jsonreachweaver campaign-preview --file F --brand B --accounts config/accounts.json --utm --markdown > review.mdreachweaver campaign-preview --file F --brand B --json| Option | Effect |
|---|---|
--file F |
Campaign JSON. It is read only. |
--brand B |
Brand JSON, including the optional per-account rules described below. |
--accounts A |
Optional public accounts config. It is loaded only when you pass it (the global --accounts works too). It enables account/platform checks and a Mastodon account’s lower max_characters. |
--utm |
Shows every link with utm_source=<platform>, utm_medium=social, utm_campaign=<campaign_id> and utm_content=<variant id> next to the original, plus the length after tagging. |
--optout F |
Opt-out list (default $REACHWEAVER_OPTOUT, else config/optout.json if present). An @-mention of a listed handle or domain in the text, any thread part or any carousel slide is an optout_mention error. See opt-out list. |
--history-db DB |
Optional existing local database, opened read-only. Adds history_near_duplicate and history_frequency warnings against the same account’s posts, and a history summary. Tune with --history-days, --history-window-days and --history-max-posts. See posting history. |
--json / --markdown |
Machine-readable report, or an editable Markdown review sheet. The default is compact text. |
The exit code is 2 when any variant has low confidence or the campaign has campaign-level errors, and 0 otherwise. A 0 exit means the variant is ready for review. It does not mean the variant is authorized to publish.
What each variant shows
Section titled “What each variant shows”- Exact text.
textis the full body as typed.pieces[]splits it into a text post, the parts of a thread, or a carousel caption and slides. - Platform-accurate length and remaining budget (
pieces[].length):- Bluesky counts approximate grapheme clusters (never under-counted) against 300 and UTF-8 bytes against 3000. Both budgets are shown.
- Mastodon counts code points with each URL counted as at least 23. The limit is the account’s
max_characterswhen--accountssets a lower value, and the conservative cap of 450 otherwise.limit_sourcesays which one applied. - Every other platform counts Unicode code points against ReachWeaver’s conservative cap (
policy.CONSERVATIVE_TEXT_LIMITS). That cap is not a provider spec.
- Facets, hashtags, mentions and links (
pieces[].features). Bluesky adds link and tag facets at UTF-8 byte offsets. Mentions stay plain text because ReachWeaver does no handle-to-DID resolution. - Capability status from
platform_registry.json:api_direct,gated(api_gated),manual(manual_export),unavailableorunknown, plus whether a ReachWeaver live text adapter exists. This is dated research. It is not consent and not account authorization. - Evidence links.
evidence[]lists the cited evidence records.grounded[]lists each figure or phrase that claims.py matched, with theevidence_urlsit is anchored on. - Grounding findings from claims.py (see claim grounding), plus the preview’s own thread and carousel checks. For a thread or carousel,
piecenames the part or slide a finding falls in. - Brand rules that apply to the variant’s account (
brand_rules): the merged banned phrases, required disclosures, hashtag cap and hashtag count. - Stageability.
stageableis true only for atextvariant with no errors. Thread and carousel variants are creative briefs and are never staged. Staging still needscampaign-stageand a private accounts file.
Confidence
Section titled “Confidence”confidence.level is computed from rules. No model is involved:
| Level | Rule |
|---|---|
high |
No blocking errors and no human-review items, so every figure is grounded in cited evidence. |
medium |
Human-review items are present (testimonials, endorsements, customer counts, superlatives, feature claims missing from cited evidence, a thread whose first part does not stand alone). |
low |
Any blocking error: an unsupported figure or release claim, a banned phrase, a missing disclosure, too many hashtags, over the length limit, a structural error, or a campaign-level error. |
Warnings (near_duplicate, history_near_duplicate, history_frequency, missing_cta, carousel_unavailable, a long first line) are listed in the rationale but do not change the level. confidence.rationale explains the level line by line. The rationale is editable in two ways:
- An optional variant field
rationale(a nonempty string, at most 1000 characters) records your own editorial reasoning. It is shown verbatim aseditor_rationaleand never changes the computed level. --markdownwrites the rationale as bullets under Rationale (editable) for you to revise in the review sheet. The sheet is not read back.
Confidence inherits every limit of lexical grounding. high means the checked tokens have anchors in the evidence you typed. It does not mean the post is true.
Threads and carousel outlines (optional, backwards compatible)
Section titled “Threads and carousel outlines (optional, backwards compatible)”{"id": "guardrails-thread", "platform": "bluesky", "format": "thread", "parts": ["Hook that stands alone, with the link: https://github.com/MSKazemi/reachweaver", "Second post", "Third post"], "account_id": "my-bluesky", "angle": "...", "cta": "...", "evidence_ids": ["source-repo"]}{"id": "gates-carousel", "platform": "linkedin", "format": "carousel_outline", "text": "Optional caption", "slides": ["Slide one", "Slide two"], "account_id": "my-linkedin", "angle": "...", "cta": "...", "evidence_ids": ["source-repo"]}threadneeds 2 to 25 stringpartsand notext. Each part is measured and validated against the platform limit on its own, and labelled1/Nfor review. ReachWeaver inserts no numbering. If you type numbering such as2/4into a part, it is grounded like any other figure. The first part must stand alone: its first line, after a leading 🧵,1/orthread:marker, needs at least 4 words (the hook), and the part must contain a link or the variant’scta_phrase. If it does not, the preview reports athread_first_part_not_standalonereview item. Grounding runs over all parts.carousel_outlineneeds 1 to 20 slides, each a nonempty string of at most 2000 characters, plus an optionaltextcaption that is measured against the platform limit. The preview compares the outline with the registry’scarouselstatus. It warnscarousel_unavailablewhen the status isunavailable(for example, LinkedIn organic carousels) andcarousel_unverifiedwhen it isunknown. The registry holds no per-platform slide limits, so none are invented.- Both formats still produce the unchanged error
runtime only stages text posts; other formats are creative briefsincampaign-check,campaign-stageandcampaign-export. Only text variants are staged, as before.
Per-account brand rules
Section titled “Per-account brand rules”An optional brand accounts object adds rules for individual approved accounts:
{"banned_phrases": ["game-changer"], "accounts": {"my-linkedin": {"banned_phrases": ["thought leader"], "max_hashtags": 3, "required_disclosure": "Disclosure: I maintain ReachWeaver"}}}The merge is additive and never weakens a global rule. Banned phrases are the union of both lists. Every distinct required disclosure must appear. The hashtag cap is the lowest one set, so a global brand max_hashtags (an integer from 0 to 30) also works. Findings that come from an account override carry scope: "account:<id>". When accounts is absent, claims checks behave exactly as before. Validation rejects an override whose key is not in approved_accounts, any unknown field, and any invalid value. Hashtags are counted outside links with Bluesky tag syntax, and an overlong tag still counts.
Known limits
Section titled “Known limits”- The hook and stand-alone checks are word counts and the presence of a link or phrase. They do not judge whether a hook is good.
- Capability statuses are only as current as the registry’s
verified_date. Runreachweaver capabilitiesto check staleness. - UTM tagging only covers
httpslinks. Any other link is reported with the error and left unchanged. - Lengths for platforms other than Bluesky and Mastodon use conservative code-point caps, not each provider’s exact counting rules.
