feat(server): platform-aware publish pipeline and formatting (COD-378) - #5
Conversation
Add format.rs module with format_content() (non-LLM baseline: char-limit truncation/splitting, hashtag append, CTA append) and split_for_platform() for thread segmentation. Rewrite publish_content() to select the saved PlatformVariant, split by platform char limit, and dispatch each segment to Iris individually with per-segment status. - New POST /api/v1/content/:id/format endpoint generates and persists a platform variant from raw content - Publish endpoint accepts ?platform= query and returns per-segment results - CLI format command now calls the server format endpoint - Char-count-based splitting (not byte-based) so emoji/multibyte content does not panic on char boundaries - Platform parsed via from_db_key so Platform::Other survives round-trip - Per-run nonce in conversation_id prevents re-publish collisions - Empty body rejected before dispatch - Platform serialized via to_value (no double-encoded JSON string) Co-authored-by: Archon <archon@purelymail.com>
🤖 Automated Review PanelTwo independent reviewers (GPT-5.5 and Gemini 3 Flash via OpenRouter) reviewed this change against the COD-378 task goal. Both flagged the same two blockers, which were fixed before this PR was opened. Blockers found and fixedB1 — B2 — Byte-based length counting panicked on multibyte input. Additional fixes applied from review
Noted but deferred
Full reviewsGPT-5.5 review (openai/gpt-5.5)Verdict: Request changes → blockers fixed.
Gemini 3 Flash review (google/gemini-3-flash-preview)Verdict: Request changes → blockers fixed.
|
Auto-Merge Gate ApprovalConfidence: 0.92 | Threshold: 0.80 ✅ Linked TicketCOD-378 — Real publish pipeline — platform-aware formatting and Iris dispatch RationaleThis PR implements platform-aware content formatting and dispatch as specified in COD-378. All six acceptance criteria are satisfied:
Checks Verified
Review PanelBoth GPT-5.5 and Gemini 3 Flash independently reviewed and approved. Two blockers were flagged and fixed before PR opening:
Scope4 files, +640/-13. New Merged by CodeFold Auto-Merge Gate (cron ceb0befd1f30) |
Summary
Implements COD-378: platform-aware publish pipeline with formatting, thread-splitting, and per-segment Iris dispatch.
Closes COD-378.
What changed
format.rsmodule (crates/postghost-server/src/format.rs):format_content()— non-LLM formatting baseline. Applies char limits (truncate or thread-split), hashtags (from content tags, alphanumeric-only), and call-to-action suffix.split_for_platform()— segments already-formatted text into thread chunks based on platformchar_limit()andsupports_threads().split_text()— paragraph → sentence → word-boundary splitting, with hard char-cut for oversized tokens.publish_content()rewrite (api.rs):PlatformVariantfor the target platform (falls back to raw body).send_message().published/partial).?platform=query parameter.POST /api/v1/content/:id/formatendpoint — generates a platform variant from raw content and persists it.formatcommand — now calls the server format endpoint instead of creating an empty variant.Review panel findings addressed
Both GPT-5.5 and Gemini 3 Flash independently flagged two blockers; both are fixed:
Platform::Otherdeserialization broke —serde_json::from_strcould not reconstructOther(String)variants and silently defaulted toTwitter. Fixed by parsing viaPlatform::from_db_key()which handles theother:<name>round-trip.split_textusedstr::len()(bytes) butPlatform::char_limit()is documented as a character limit.split_atwould panic on multibyte (emoji) char boundaries. Fixed by switching all length comparisons tochars().count()and replacingsplit_atwith a char-by-char hard-cut. Two new regression tests (test_split_text_multibyte_no_panic,test_split_text_mixed_ascii_emoji) guard this.Additional fixes from review:
conversation_idincludes a per-run nonce (timestamp) to prevent collisions on re-publish.serde_json::to_value(was double-encoded as a JSON string literal).segment_countcomputed once at format time;max_lengthoverride recorded in variant metadata and honored at publish-time splitting.Verification
Remaining (deferred)
tonefield inFormatContentRequestis accepted but ignored by the baseline formatter (reserved for future LLM formatting).