openapi: "3.1.0" info: title: HelixForge Telegram Publishing version: "R-1" description: | Publishes composed channel posts to a Telegram channel. The unit of publication is a POST, not a hall entry. A post is a short, personal, reach-optimized piece written *from* a hall-of-helix entry — it is not the entry, and it is not a truncation of the entry. Composition happens upstream, before this interface is called. That is a deliberate architectural boundary and not an accident of layering: composing a condensed, entertaining post is a probabilistic task, and ArchitectureBlueprint.md section 48.1 names an LLM in the mandatory request path as an anti-pattern. This interface stays deterministic by taking the composed text as input. Publication is idempotent on post_id, because the caller is automation that may be retried and a duplicate post cannot be quietly undone. paths: /v1/channel-posts: post: operationId: publishPost summary: Publish a composed channel post requestBody: required: true content: application/json: schema: type: object required: [post_id, body, source_entry_id, source_entry_url, reviewed_by] additionalProperties: false properties: post_id: type: string minLength: 1 maxLength: 128 description: | Stable id for this post. Publication is idempotent on it. A recomposition of the same entry is a NEW post_id, because it is a different piece of writing about the same work. body: type: string minLength: 1 maxLength: 4096 description: | The composed post, already within Telegram's single-message limit. Bounded here on purpose: a composition that does not fit was not condensed, and this interface must not decide on the composer's behalf what to cut. source_entry_id: type: string minLength: 1 description: | The hall entry this post was written from. Required: a post with no traceable source cannot be checked against what the person actually said about their own work. source_entry_url: type: string minLength: 1 description: Canonical location of the source entry. reviewed_by: type: string minLength: 1 description: | Who approved this composition for publication. Required at FLUID-2 and above: the post speaks about a named person's work in HelixForge's voice, and no generated text reaches subscribers without someone having read it. subject_display_name: type: string maxLength: 128 description: The person or agent whose work the post is about. subject_consent: type: string enum: [explicit, standing, not-required] description: | On what basis this post may speak about the subject's work. "standing" covers contributors who agreed when they took a seat in the hall; "explicit" is per-post approval. variant: type: string maxLength: 64 description: | Which composition strategy produced this post. Opaque to this interface: it records the label and reports engagement against it, so that the campaign can compare strategies it owns without this interface needing to understand them. visual_url: type: string description: Portrait or illustration to attach. tags: type: array maxItems: 8 items: {type: string, maxLength: 32} responses: "201": description: Published "200": description: Already published; the existing publication is returned "400": description: The post cannot be published as given "403": description: Publication refused; no review or no basis for consent "502": description: Telegram was unreachable or refused the message get: operationId: listPosts summary: List publications, newest first parameters: - name: limit in: query schema: {type: integer, minimum: 1, maximum: 100} - name: state in: query schema: type: string enum: [pending, published, failed, superseded] - name: variant in: query schema: {type: string, maxLength: 64} - name: source_entry_id in: query schema: {type: string} responses: "200": description: Publications /v1/channel-posts/{post_id}: get: operationId: getPost summary: Publication state for one post parameters: - name: post_id in: path required: true schema: {type: string, minLength: 1} responses: "200": description: Publication state "404": description: This post has not been published