fluid-telegram/contracts/r1.openapi.yaml
tegwick 3aa8cb7eaf Reframe the interface around composed posts, not republished entries
The channel publishes a post written from a hall entry -- condensed,
personal, reach-optimized -- not the entry itself. That changes what
this interface is for and where its boundary sits.

Composition is deliberately outside the interface. Composing is a
probabilistic editorial task, and Blueprint 48.1 forbids an LLM in the
mandatory request path, so an interface that composed on demand would be
non-deterministic exactly where determinism is the whole claim. It is
also editorial: a post speaks about a named person's work in
HelixForge's voice, and a review step inside the request path is a
review step nobody performs.

The contract now takes a composed post with a required reviewer, a cited
source entry and a consent basis. A post carrying none of those is
refused. Absence of objection is not consent, and in an audit trail an
unfaithful post looks exactly like a faithful one -- the defence is that
a human read it, the source is named, and both are recorded.

The first experiment is rebuilt accordingly. The old competition asked
how to fit a whole entry into Telegram; that question no longer exists.
The new one asks whether the portrait earns the 3072 characters of
caption limit it costs, which is a delivery question this interface
actually owns. Competition over editorial voice belongs to the campaign
and runs on the per-variant engagement this interface reports.

Adds T05b (composition and review), T05c (consent basis for the 94
existing participants, blocking public publication) and T11 (extracting
the campaign into pr-hall-of-helix).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1116572@bnt-lap001
Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
2026-09-04 11:57:28 +02:00

154 lines
5.7 KiB
YAML

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