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
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ enough to compare over time and no more granular than the analysis requires.
|
|||
|
||||
| Cohort | Assigned by | Why it is distinct |
|
||||
|---|---|---|
|
||||
| `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many entries at once |
|
||||
| `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One entry at a time, at session end; the richest pressure signal |
|
||||
| `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many posts at once. Becomes the `pr-hall-of-helix` campaign job |
|
||||
| `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One post at a time, at session end |
|
||||
| `operators` | `X-FLUID-Consumer` prefix `op-` | Rare, deliberate, often corrective |
|
||||
| `telegram-subscribers` | Not a caller | An observed population, never an API consumer |
|
||||
|
||||
|
|
@ -31,9 +31,10 @@ Beyond the fluid-core defaults, this interface adds:
|
|||
usernames, display names of subscribers — must not reach the evidence store
|
||||
in any form, redacted or otherwise. There is no legitimate analysis that
|
||||
needs them, so the safe rule is that they never arrive.
|
||||
- **Entry content is not telemetry.** An entry body may appear in a request; it
|
||||
must not appear in a telemetry event. What is recorded is its length, its
|
||||
section count, and whether conversion succeeded.
|
||||
- **Post text is not telemetry.** The body appears in the request and is kept
|
||||
in the publication record, because it is what was said in HelixForge's name.
|
||||
It must not appear in a telemetry event: observation is for measuring how
|
||||
posts performed, not for accumulating a second copy of what they said.
|
||||
- **The bot token appears nowhere.** Not in errors, not in traces, not in
|
||||
redacted form.
|
||||
|
||||
|
|
@ -55,16 +56,32 @@ loss and start a fresh evidence baseline, not to rotate quietly.
|
|||
|
||||
| Metric | Role | Source |
|
||||
|---|---|---|
|
||||
| `read_through_rate` | primary | Telegram channel statistics, aggregate |
|
||||
| `messages_per_entry` | guardrail | Adapter |
|
||||
| `entry_content_loss` | guardrail | Adapter |
|
||||
| `engagement_rate` | primary | Telegram channel statistics, aggregate |
|
||||
| `unreviewed_publication` | guardrail | Adapter |
|
||||
| `missing_source_attribution` | guardrail | Adapter |
|
||||
| `missing_subject_consent` | guardrail | Adapter |
|
||||
| `error_rate` | guardrail | Gateway |
|
||||
| `reaction_rate` | secondary | Telegram channel statistics, aggregate |
|
||||
| `time_to_first_read` | learning | Telegram channel statistics, aggregate |
|
||||
| `source_link_rate` | secondary | Telegram channel statistics, aggregate |
|
||||
| `forward_rate` | secondary | Telegram channel statistics, aggregate |
|
||||
| `time_to_first_view` | learning | Telegram channel statistics, aggregate |
|
||||
|
||||
`read_through_rate` is the one to be careful about. Telegram reports view counts
|
||||
per message, not per reader. For the teaser form it is link clicks over views;
|
||||
for the serialized form it is views of the last part over views of the first.
|
||||
Those are not the same measurement, and comparing them directly is the weakest
|
||||
part of the first experiment. Say so in the experiment's amendment record rather
|
||||
than letting the comparison look cleaner than it is.
|
||||
`engagement_rate` is the one to be careful about. Telegram reports views,
|
||||
reactions and forwards per message, not per reader, and a "rate" therefore has
|
||||
subscribers as its denominator rather than people who saw the post. It is a
|
||||
proxy, and a noisy one at small subscriber counts. Say so in the experiment's
|
||||
amendment record rather than letting the comparison look cleaner than it is.
|
||||
|
||||
## Variant attribution
|
||||
|
||||
Every post carries a `variant` label naming the composition strategy that
|
||||
produced it. This interface treats it as opaque: it records the label, and
|
||||
reports engagement grouped by it.
|
||||
|
||||
That is deliberately the whole of the interface's involvement in composition.
|
||||
The campaign compares its own strategies using this data, running its own FLUID
|
||||
experiments over its own artifacts. The interface does not know what
|
||||
`variant: warm-specific-v2` means and must not acquire an opinion about it.
|
||||
|
||||
Without this field the campaign would have to infer which post came from which
|
||||
strategy by timestamp, which is exactly the kind of reconstruction that quietly
|
||||
stops being accurate.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue