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
This commit is contained in:
parent
aa95a09ab6
commit
3aa8cb7eaf
14 changed files with 793 additions and 521 deletions
|
|
@ -7,18 +7,22 @@ API.
|
|||
|
||||
## What the adapter owns
|
||||
|
||||
- Rendering a hall entry into Telegram's message forms.
|
||||
- Rendering a **composed post** into a Telegram message.
|
||||
- Attaching the post's visual, where the revision calls for one.
|
||||
- Talking to the Telegram Bot API.
|
||||
- Custody of the bot token for the duration of a call.
|
||||
- Idempotency: recognising an `entry_id` it has already published.
|
||||
- Idempotency: recognising a `post_id` it has already published.
|
||||
|
||||
## What the adapter must not do
|
||||
|
||||
- Create, rename or delete channels, groups or bots.
|
||||
- Change membership, administrators or permissions.
|
||||
- Post to any chat other than the one it is configured with.
|
||||
- Edit entry content to make it fit. If an entry cannot be published
|
||||
faithfully, refuse it — that refusal is the pressure signal.
|
||||
- **Compose, rewrite, shorten or embellish post text.** The post arrives
|
||||
written. If it does not fit, refuse it: a composition that exceeds the limit
|
||||
was not condensed, and deciding what to cut is the composer's judgement.
|
||||
- Publish a post carrying no `reviewed_by`, or none of the recognised consent
|
||||
bases. Refuse with 403.
|
||||
- Log, echo or forward the bot token, including in error messages.
|
||||
|
||||
## Endpoints
|
||||
|
|
@ -27,6 +31,10 @@ Exactly the three operations in `contracts/r1.openapi.yaml`, at the same paths.
|
|||
The gateway validates requests against that contract before the adapter sees
|
||||
them, so the adapter may assume shape but must still assume hostile content.
|
||||
|
||||
Note what the contract does **not** accept: a hall entry. The adapter never
|
||||
reads `hall-of-helix`. Its only knowledge of an entry is the id and URL a post
|
||||
cites, which it renders as a link.
|
||||
|
||||
## Backend capability contract
|
||||
|
||||
| Property | Value |
|
||||
|
|
@ -42,49 +50,68 @@ them, so the adapter may assume shape but must still assume hostile content.
|
|||
|
||||
## The idempotency requirement
|
||||
|
||||
`POST /v1/hall-entries` is idempotent on `entry_id`. The caller is automation
|
||||
that may be retried, and a duplicate hall entry in the channel is not something
|
||||
you can quietly undo — subscribers have already seen it.
|
||||
`POST /v1/channel-posts` is idempotent on `post_id`. The caller is automation
|
||||
that may be retried, and a duplicate post is not something you can quietly undo
|
||||
— subscribers have already seen it.
|
||||
|
||||
The adapter keeps its own mapping from `entry_id` to the Telegram message ids it
|
||||
The adapter keeps its own mapping from `post_id` to the Telegram message id it
|
||||
produced. On a repeat call it returns `200` with the existing publication rather
|
||||
than posting again. If the entry content has changed, it edits the existing
|
||||
messages rather than posting new ones.
|
||||
than posting again. If the body has changed, it edits the existing message.
|
||||
|
||||
A **recomposition** of the same source entry is a new `post_id`, not an edit.
|
||||
It is a different piece of writing about the same work, and collapsing the two
|
||||
would lose the comparison between them.
|
||||
|
||||
This state is the adapter's, not fluid-core's. The evidence store records that a
|
||||
publication happened; the adapter records which Telegram messages implement it.
|
||||
|
||||
## Rendering rules
|
||||
|
||||
Telegram formatting is not Markdown. Entry Markdown must be converted, not
|
||||
Telegram formatting is not Markdown. Post Markdown must be converted, not
|
||||
passed through.
|
||||
|
||||
| Entry construct | Telegram |
|
||||
| Post construct | Telegram |
|
||||
|---|---|
|
||||
| `# Heading` | Bold line |
|
||||
| `**bold**`, `_italic_` | `<b>`, `<i>` in HTML parse mode |
|
||||
| Fenced code | `<pre>` |
|
||||
| Tables | Flattened to lines; Telegram has no table |
|
||||
| Links | `<a href>` |
|
||||
| Frontmatter | Stripped; `display_name` and `recorded_at` surfaced in the header |
|
||||
| Source link | Appended as an attributed link to `source_entry_url` |
|
||||
|
||||
Anything the converter cannot represent faithfully must fail the publication
|
||||
rather than degrade it silently. `entry_content_loss` is a hard guardrail in
|
||||
both competing hypotheses, and it can only be honest if the adapter refuses
|
||||
rather than approximates.
|
||||
rather than degrade it silently.
|
||||
|
||||
Two constants regardless of revision: every post carries a link to its source
|
||||
entry, and every post is attributable. `missing_source_attribution` is a hard
|
||||
guardrail, and a post that cannot be checked against what the person actually
|
||||
wrote is the failure this whole arrangement exists to prevent.
|
||||
|
||||
**Caption limit.** When the revision attaches a visual (R-2), Telegram caps the
|
||||
caption at 1024 characters rather than 4096. The adapter must refuse a post
|
||||
that exceeds the limit for the form it is publishing in, rather than truncating
|
||||
it — the difference between those two limits is the entire subject of the first
|
||||
experiment, and silently trimming would destroy the measurement.
|
||||
|
||||
## Telemetry the adapter should emit
|
||||
|
||||
The gateway observes the request path already. The adapter adds what only it can
|
||||
see, by POSTing to the control plane's `/control/v1/telemetry`:
|
||||
|
||||
- `messages_per_entry` — how many Telegram messages one entry became.
|
||||
- `entry_content_loss` — whether the conversion dropped anything.
|
||||
- `variant` — the composition strategy label the post carried, so engagement
|
||||
can be attributed to it. This is what lets the campaign compare strategies it
|
||||
owns without this interface understanding them.
|
||||
- `unreviewed_publication` — always false in practice, since such a post is
|
||||
refused; emitted so the guardrail has a series rather than an absence.
|
||||
- `missing_source_attribution`, `missing_subject_consent` — likewise.
|
||||
- Telegram rate-limit encounters, as error events with class `backend_failure`.
|
||||
|
||||
It must not emit subscriber identity in any form. Engagement metrics
|
||||
(`read_through_rate`, `reaction_rate`) come from Telegram's channel statistics
|
||||
as aggregates, never per-reader.
|
||||
It must not emit subscriber identity in any form, and it must not emit post
|
||||
body text as telemetry — the body belongs in the publication record, not in the
|
||||
observation stream.
|
||||
|
||||
Engagement metrics (`engagement_rate`, `source_link_rate`, `forward_rate`) come
|
||||
from Telegram's channel statistics as aggregates, never per-reader.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -94,6 +121,7 @@ as aggregates, never per-reader.
|
|||
| `TELEGRAM_CHANNEL_ID` | The channel this adapter publishes to |
|
||||
| `FLUID_CONTROL_URL` | Where to POST telemetry |
|
||||
| `HALL_BASE_URL` | Base URL for links back to hall-of-helix |
|
||||
| `ATTACH_VISUAL` | Whether this revision attaches the portrait (R-2 true, R-3 false) |
|
||||
|
||||
One adapter instance serves one channel. The private test channel and the public
|
||||
channel are separate instances with separate configuration, which is what keeps
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue