Compare commits

..

No commits in common. "ca47f7879dd8a1d461d1649f4db1ecd6c9c1eb6f" and "aa95a09ab6d461b52e8775da0812c55e6c716fdc" have entirely different histories.

14 changed files with 529 additions and 804 deletions

View file

@ -59,37 +59,21 @@ Raising the mode is a governance change under §20, not an operational decision.
### Mission statement ### Mission statement
> Deliver composed channel posts to a Telegram audience reliably, verifiably, > Make hall-of-helix entries reliably and faithfully available to a Telegram
> and without an unreviewed word ever reaching a subscriber. > audience, without the people writing them having to think about Telegram.
The unit of publication is a **post**, not a hall entry. A post is short, An entry is written once, as Markdown, in `hall-of-helix/entries/`. Everything
personal and written to travel; a hall entry is a considered first-person about how it becomes a Telegram post — length, formatting, media, threading —
account written for someone reading a repository. They are different pieces of is this interface's problem and nobody else's.
writing for different readers, and the second is the source of the first rather
than its content.
**Composition is not this interface's job.** Turning an entry into a post that
is condensed, entertaining and reach-optimized is a probabilistic, editorial
task. It happens upstream, before this interface is called. Two reasons, and
the first is architectural rather than stylistic:
1. `ArchitectureBlueprint.md` §48.1 names an LLM in the mandatory request path
as an anti-pattern. An interface that composed on demand would be
non-deterministic at exactly the point where determinism is the whole claim.
2. A composed post speaks about a named person's work in HelixForge's voice.
That deserves a human read before it goes out, and a review step in the
request path is a review step nobody performs.
### Success looks like ### Success looks like
- A composed post reaches the channel without a human formatting it for - A finished hall entry reaches the channel without a human reformatting it.
Telegram. - The published form is faithful: no entry is silently truncated, and no
- No post reaches a subscriber unreviewed. attribution is lost.
- Every post is traceable to the entry it was written from, so a claim can be
checked against what the person actually said.
- Publishing twice does not post twice. - Publishing twice does not post twice.
- Engagement is attributable to the composition strategy that produced it, so - When Telegram's constraints and the entry's shape conflict, the conflict
the campaign can learn which writing travels. surfaces as recorded pressure rather than as a mangled post.
--- ---
@ -121,13 +105,10 @@ the first is architectural rather than stylistic:
### In scope ### In scope
- Publishing composed posts to a Telegram channel (Canon PUB-01). - Publishing hall-of-helix entries to a Telegram channel (Canon PUB-01).
- Rendering a post into Telegram's message forms (Canon PUB-02, MED-02). - Rendering an entry into Telegram's message forms (Canon PUB-02, MED-02).
- Attaching a visual to a post (Canon MED-01).
- Publication state: what was published, when, as which message. - Publication state: what was published, when, as which message.
- Idempotency and correction of previously published posts. - Idempotency and correction of previously published entries.
- Recording the composition variant a post came from, and reporting engagement
against it.
- Bot identity presentation (Canon BOT-01). - Bot identity presentation (Canon BOT-01).
### Conditionally in scope ### Conditionally in scope
@ -135,14 +116,12 @@ the first is architectural rather than stylistic:
Capabilities that may be introduced if evidence justifies them: Capabilities that may be introduced if evidence justifies them:
- Scheduling a publication for a later time. - Scheduling a publication for a later time.
- Publishing other HelixForge material, such as release notes. - Publishing other HelixForge artifacts, such as release notes.
- Attaching entry visuals as media (Canon MED-01).
- A linked discussion group for comments. - A linked discussion group for comments.
- Publishing to more than one channel from one call.
### Out of scope ### Out of scope
- **Composing post text from a source entry.** This belongs to the campaign
that owns the editorial voice, not to the interface that delivers it. See §7.
- Inbound bot commands and conversation (Canon BOT-02, AGT-01). A second - Inbound bot commands and conversation (Canon BOT-02, AGT-01). A second
revision family may add this later; it is not this interface today. revision family may add this later; it is not this interface today.
- Payments and paid content (Canon PAY-*). - Payments and paid content (Canon PAY-*).
@ -156,28 +135,20 @@ Capabilities that may be introduced if evidence justifies them:
### Responsibilities owned by this interface ### Responsibilities owned by this interface
- Representation: how a composed post becomes a Telegram message. - Representation: how an entry becomes one or more Telegram messages.
- Delivery: attaching the visual, the source link, the formatting. - Composition: assembling an entry, its metadata and its visual.
- Validation: refusing a post that arrives unreviewed, unattributed, or in a - Validation: refusing an entry that cannot be published faithfully.
form Telegram cannot carry.
- Routing: which revision serves a publication request. - Routing: which revision serves a publication request.
- Idempotency: recognising a post it has already published. - Idempotency: recognising an entry it has already published.
- Attribution of engagement to composition variants.
### Responsibilities owned by backend systems ### Responsibilities owned by backend systems
| Backend | Responsibility | Boundary | | Backend | Responsibility | Boundary |
|---|---|---| |---|---|---|
| Telegram Bot API | Message delivery, channel state, subscriber identity | FLUID may request delivery. It may not assume a limit will move, and must treat platform constraints as fixed. | | Telegram Bot API | Message delivery, channel state, subscriber identity | FLUID may request delivery. It may not assume a limit will move, and must treat platform constraints as fixed. |
| `pr-hall-of-helix` (planned) | Composing posts, the editorial voice, choosing what is worth publishing, obtaining subject consent | FLUID publishes what it is given and reports how it performed. It does not write, edit or select. | | `hall-of-helix` | Entry content, its correctness, and whether it is finished | FLUID may read entries. It must never edit one to make it publishable. |
| `hall-of-helix` | Entry content, its correctness, and whether it is finished | FLUID never reads or edits an entry. Its only knowledge of one is the id and URL a post cites. |
| OpenBao | Custody of the bot token | FLUID receives a credential. It never stores, logs or forwards one. | | OpenBao | Custody of the bot token | FLUID receives a credential. It never stores, logs or forwards one. |
Until `pr-hall-of-helix` exists, composition is done by hand as part of
`FT-WP-0001`. That is a temporary arrangement, and the boundary above is
written for the state it is heading toward rather than the state it is in — so
that the campaign, when it is extracted, finds the seam already cut.
### Explicit non-authority ### Explicit non-authority
The Daimon must not autonomously: The Daimon must not autonomously:
@ -185,20 +156,14 @@ The Daimon must not autonomously:
- create, rename or delete Telegram channels, groups or bots; - create, rename or delete Telegram channels, groups or bots;
- change channel membership, administrators or permissions; - change channel membership, administrators or permissions;
- read, rotate or re-scope the bot token; - read, rotate or re-scope the bot token;
- compose, rewrite, shorten or embellish post text; - edit or truncate a hall-of-helix entry to fit a platform constraint;
- publish a post that carries no reviewer;
- publish to any chat other than those declared in the routing configuration; - publish to any chat other than those declared in the routing configuration;
- publish a post about a person who has not consented, on either basis in §15. - post on behalf of a person who has not asked for it.
The last two matter most, and they are the reason composition sits outside this The last of these matters most. A hall entry is a first-person account someone
interface. A post is a condensation written in HelixForge's voice about a named wrote about their own work. Publishing it is an act taken on their behalf, and
person's work. It can flatter, it can misstate, and it can put a claim in that authority comes from their having written it — not from the interface
someone's mouth that their own entry never made — and in the audit trail an finding it convenient.
unfaithful post looks exactly like a faithful one.
The defence is not that the composer is careful. It is that a human read the
post, the post names the entry it came from, and both are recorded before
anything reaches a subscriber.
--- ---
@ -206,29 +171,23 @@ anything reaches a subscriber.
### Concepts consumers should naturally understand ### Concepts consumers should naturally understand
- A **post** is the unit of publication, identified by its own post id. - An **entry** is the unit of publication, identified by its hall entry id.
- A post **cites** the hall entry it was written from; it is not that entry. - Publishing is **idempotent** on that id.
- Publishing is **idempotent** on the post id. A recomposition is a new post,
because it is a different piece of writing about the same work.
- A **publication** has state: pending, published, failed, superseded. - A **publication** has state: pending, published, failed, superseded.
### Interaction patterns considered natural ### Interaction patterns considered natural
- Compose a post, have it reviewed, publish it, check its state. - Publish one finished entry, then check its state.
- Re-publish the same post id after a correction and get the same message - Re-publish the same entry after a correction and get the same message updated.
updated. - List what has been published to reconcile against the hall.
- List publications by composition variant to compare how they performed.
### Expectations that should be discouraged ### Expectations that should be discouraged
- That Telegram formatting is Markdown. It is not, and posts that assume so - That Telegram formatting is Markdown. It is not, and entries that assume so
will render wrongly. will render wrongly.
- That a post appears instantly. Rate limits are real. - That an entry appears instantly. Rate limits are real.
- That this interface will shorten an over-length post. It will refuse it. A - That a long entry arrives as one message. It cannot, and the interface's job
composition that does not fit was not condensed, and deciding what to cut is is to make that invisible rather than to pretend otherwise.
the composer's judgement, not the deliverer's.
- That publishing a hall entry is possible. It is not: entries are sources, and
posts are what this interface carries.
--- ---
@ -244,10 +203,10 @@ volume.
| Dimension | Priority | Target / Constraint | Notes | | Dimension | Priority | Target / Constraint | Notes |
|---|---:|---|---| |---|---:|---|---|
| Correctness | High | No post published unreviewed or unattributed | Hard guardrail | | Correctness | High | No entry published in a form that loses content | Hard guardrail |
| Client utility | High | One call publishes one post | The reason the interface exists | | Client utility | High | One call publishes one entry | The reason the interface exists |
| Reliability | High | Publication succeeds or reports why | | | Reliability | High | Publication succeeds or reports why | |
| Discoverability | High | A reader can reach the source entry | Drives the first experiment | | Discoverability | High | A reader can reach the full entry | Drives the first experiment |
| Security | High | Token never leaves its custody | Hard guardrail | | Security | High | Token never leaves its custody | Hard guardrail |
| Compatibility | Medium | Versioned evolution | Few consumers, all internal | | Compatibility | Medium | Versioned evolution | Few consumers, all internal |
| Simplicity | Medium | Prefer one publication concept | | | Simplicity | Medium | Prefer one publication concept | |
@ -267,7 +226,7 @@ volume.
| Dimension | Current | Preferred | Maximum | Scoring method | | Dimension | Current | Preferred | Maximum | Scoring method |
|---|---:|---:|---:|---| |---|---:|---:|---:|---|
| Surface area (operations) | 3 | 4 | 8 | Count of declared operations | | Surface area (operations) | 3 | 4 | 8 | Count of declared operations |
| Concept count | 2 | 3 | 5 | post, publication | | Concept count | 2 | 3 | 5 | entry, publication |
| Parameter dimensionality | 4 | 6 | 12 | Declared parameters across operations | | Parameter dimensionality | 4 | 6 | 12 | Declared parameters across operations |
| Semantic overlap | 0 | 0 | 1 | Operations meaning the same thing | | Semantic overlap | 0 | 0 | 1 | Operations meaning the same thing |
| Alternative-path count | 1 | 1 | 3 | Ways to achieve one outcome | | Alternative-path count | 1 | 1 | 3 | Ways to achieve one outcome |
@ -283,9 +242,7 @@ volume.
another endpoint. another endpoint.
- A candidate exceeding a hard maximum must not be promoted. - A candidate exceeding a hard maximum must not be promoted.
- Presentation adaptations do not consume surface-area budget: they change how - Presentation adaptations do not consume surface-area budget: they change how
a post is delivered, not what the interface offers. an entry is rendered, not what the interface offers.
- Any candidate that would move composition inside this interface is rejected
regardless of its complexity score. That boundary is not a budget question.
--- ---
@ -301,9 +258,8 @@ volume.
- Breaking changes require a successor revision. - Breaking changes require a successor revision.
- Every consumer is internal and known, so a breaking change is a coordination - Every consumer is internal and known, so a breaking change is a coordination
problem rather than a public one — but it is still a successor revision. problem rather than a public one — but it is still a successor revision.
- A change to how a post is *delivered* — visual placement, link style, - A change to how an entry is *rendered* is a presentation adaptation and does
formatting — is a presentation adaptation and does not break the contract, not break the contract, even though readers will notice it.
even though readers will notice it.
### Coexistence expectations ### Coexistence expectations
@ -369,12 +325,6 @@ success criteria, guardrails, complexity impact, risk, expected learning value.
defensible answers, and choosing between them by argument rather than evidence defensible answers, and choosing between them by argument rather than evidence
is exactly the habit FLUID exists to break. is exactly the habit FLUID exists to break.
Note the division of labour. Competitions about **delivery** — does a visual
help, does a link preview help — belong to this interface and are run here.
Competitions about **composition** — which editorial voice travels — belong to
the campaign, and are run on the per-variant engagement this interface reports.
Both are legitimate FLUID experiments; only the first is this interface's.
### Reach preference ### Reach preference
Prefer the smallest sufficient adaptation. This interface is small enough that a Prefer the smallest sufficient adaptation. This interface is small enough that a
@ -428,23 +378,6 @@ population, not a caller.
Single tenant. The interface serves HelixForge and no one else. Single tenant. The interface serves HelixForge and no one else.
### Subject consent
A post speaks publicly about a named person's work. Two bases are recognised:
- **standing** — the contributor agreed, when they took a seat in the hall,
that HelixForge may write about that work publicly. This is the ordinary case
and requires the agreement to actually exist, in `hall-of-helix/ENTRY.md`, and
to be revocable.
- **explicit** — per-post approval from the subject. Required where the post
makes a claim the entry does not, quotes the subject, or concerns work they
have since asked not to be publicised.
`not-required` covers posts about the project itself with no personal subject.
A post carrying no basis is refused with 403. Absence of objection is not
consent, and the interface must not treat it as such.
### Data handling ### Data handling
Telegram user identity (Canon ID-01) must not enter the evidence store in any Telegram user identity (Canon ID-01) must not enter the evidence store in any
@ -455,13 +388,9 @@ consumers must not quietly learn about audiences.
### Logging and provenance ### Logging and provenance
Every publication records which post, which source entry, which reviewer, which Every publication records which entry, which revision, which message, and which
revision, which message, and which consumer asked. The bot token appears in no consumer asked. The bot token appears in no log, error, telemetry event or
log, error, telemetry event or evidence record. evidence record.
Post *text* is retained in the publication record. It is what was said in
HelixForge's name, and an audit that could not reproduce it would be unable to
answer the only question that matters when a post is disputed.
### AI boundary ### AI boundary
@ -475,15 +404,14 @@ reaches this interface at all.
| Quality attribute | Requirement | Hard guardrail? | | Quality attribute | Requirement | Hard guardrail? |
|---|---|---:| |---|---|---:|
| Correctness | No post published without a reviewer and a cited source entry | yes | | Correctness | No published entry loses content or attribution | yes |
| Consent | No post about a subject without a recorded basis | yes | | Idempotency | Publishing an entry twice yields one publication | yes |
| Idempotency | Publishing a post twice yields one publication | yes |
| Availability | Best effort; publication may be retried | no | | Availability | Best effort; publication may be retried | no |
| Reliability | A failed publication reports why and may be retried safely | yes | | Reliability | A failed publication reports why and may be retried safely | yes |
| p95 latency | Under 10s excluding Telegram rate limiting | no | | p95 latency | Under 10s excluding Telegram rate limiting | no |
| Error rate | Under 1% of publication attempts | yes | | Error rate | Under 1% of publication attempts | yes |
| Security | Token never leaves custody | yes | | Security | Token never leaves custody | yes |
| Auditability | Every publication traceable to post text, source entry, reviewer, revision and caller | yes | | Auditability | Every publication traceable to entry, revision and caller | yes |
| Rollback | Any revision can be replaced by its predecessor within one policy generation | yes | | Rollback | Any revision can be replaced by its predecessor within one policy generation | yes |
--- ---
@ -575,9 +503,5 @@ of scope, the first 100 subscribers, or annually.
The Daimon should optimize aggressively inside this document and remain The Daimon should optimize aggressively inside this document and remain
conservative at its boundaries. conservative at its boundaries.
This interface may evolve in how it delivers a post. It may not evolve into This interface may evolve in how it presents an entry. It may not evolve in
writing one. whose voice it speaks.
The voice belongs to the campaign, the work belongs to the person who did it,
and this interface's job is to carry the first without misrepresenting the
second.

View file

@ -9,8 +9,5 @@ live. Governance, contract, seed hypotheses and the adapter's connector contract
are in place; the bot, the channel and the adapter itself are not. are in place; the bot, the channel and the adapter itself are not.
- Governance: [`InterfaceEvolutionIntent.md`](InterfaceEvolutionIntent.md) - Governance: [`InterfaceEvolutionIntent.md`](InterfaceEvolutionIntent.md)
- The channel publishes composed posts written *from* hall entries, not the
entries themselves. Composition happens upstream and moves to
`pr-hall-of-helix` once the loop has closed once (FT-WP-0001 T13).
- Work: [`workplans/FT-WP-0001-telegram-identity-and-hall-channel.md`](workplans/FT-WP-0001-telegram-identity-and-hall-channel.md) - Work: [`workplans/FT-WP-0001-telegram-identity-and-hall-channel.md`](workplans/FT-WP-0001-telegram-identity-and-hall-channel.md)
- Handover from fluid-core: `fluid-core/docs/handover/fluid-telegram.md` - Handover from fluid-core: `fluid-core/docs/handover/fluid-telegram.md`

View file

@ -3,147 +3,96 @@ info:
title: HelixForge Telegram Publishing title: HelixForge Telegram Publishing
version: "R-1" version: "R-1"
description: | description: |
Publishes composed channel posts to a Telegram channel. Publishes hall-of-helix entries to a Telegram channel.
The unit of publication is a POST, not a hall entry. A post is a short, The interface takes an entry as it was written and is responsible for
personal, reach-optimized piece written *from* a hall-of-helix entry — it everything about how it becomes a Telegram post. A caller should never need
is not the entry, and it is not a truncation of the entry. to know Telegram's constraints.
Composition happens upstream, before this interface is called. That is a Publication is idempotent on entry_id: publishing the same entry twice
deliberate architectural boundary and not an accident of layering: yields one publication. This matters because the caller is usually
composing a condensed, entertaining post is a probabilistic task, and automation that may be retried.
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: paths:
/v1/channel-posts: /v1/hall-entries:
post: post:
operationId: publishPost operationId: publishEntry
summary: Publish a composed channel post summary: Publish a hall-of-helix entry
requestBody: requestBody:
required: true required: true
content: content:
application/json: application/json:
schema: schema:
type: object type: object
required: [post_id, body, source_entry_id, source_entry_url, reviewed_by] required: [entry_id, title, body]
additionalProperties: false additionalProperties: false
properties: properties:
post_id: entry_id:
type: string type: string
minLength: 1 minLength: 1
maxLength: 128 maxLength: 128
description: | description: |
Stable id for this post. Publication is idempotent on it. The hall entry's own id, from its frontmatter. Publication
A recomposition of the same entry is a NEW post_id, because is idempotent on this value.
it is a different piece of writing about the same work. title:
type: string
minLength: 1
maxLength: 256
body: body:
type: string type: string
minLength: 1 minLength: 1
maxLength: 4096
description: | description: |
The composed post, already within Telegram's single-message The entry body as Markdown, frontmatter removed.
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: Deliberately unbounded. Telegram's 4096-character message
type: string limit is this interface's problem, not the caller's, and a
minLength: 1 maxLength here would push it back onto them.
description: | display_name:
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 type: string
maxLength: 128 maxLength: 128
description: The person or agent whose work the post is about. worker_kind:
subject_consent:
type: string type: string
enum: [explicit, standing, not-required] enum: [human, agent-session]
description: | recorded_at:
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 type: string
maxLength: 64 format: date
description: | source_url:
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 type: string
description: Portrait or illustration to attach. description: Canonical location of the entry in hall-of-helix.
tags:
type: array
maxItems: 8
items: {type: string, maxLength: 32}
responses: responses:
"201": "201":
description: Published description: Published
"200": "200":
description: Already published; the existing publication is returned description: Already published; the existing publication is returned
"400": "400":
description: The post cannot be published as given description: The entry cannot be published faithfully
"403":
description: Publication refused; no review or no basis for consent
"502": "502":
description: Telegram was unreachable or refused the message description: Telegram was unreachable or refused the message
get: get:
operationId: listPosts operationId: listPublications
summary: List publications, newest first summary: List publications, newest first
parameters: parameters:
- name: limit - name: limit
in: query in: query
required: false
schema: {type: integer, minimum: 1, maximum: 100} schema: {type: integer, minimum: 1, maximum: 100}
- name: state - name: state
in: query in: query
required: false
schema: schema:
type: string type: string
enum: [pending, published, failed, superseded] enum: [pending, published, failed, superseded]
- name: variant
in: query
schema: {type: string, maxLength: 64}
- name: source_entry_id
in: query
schema: {type: string}
responses: responses:
"200": "200":
description: Publications description: Publications
/v1/channel-posts/{post_id}: /v1/hall-entries/{entry_id}:
get: get:
operationId: getPost operationId: getPublication
summary: Publication state for one post summary: Publication state for one entry
parameters: parameters:
- name: post_id - name: entry_id
in: path in: path
required: true required: true
schema: {type: string, minLength: 1} schema: {type: string, minLength: 1}
@ -151,4 +100,4 @@ paths:
"200": "200":
description: Publication state description: Publication state
"404": "404":
description: This post has not been published description: This entry has not been published

View file

@ -7,22 +7,18 @@ API.
## What the adapter owns ## What the adapter owns
- Rendering a **composed post** into a Telegram message. - Rendering a hall entry into Telegram's message forms.
- Attaching the post's visual, where the revision calls for one.
- Talking to the Telegram Bot API. - Talking to the Telegram Bot API.
- Custody of the bot token for the duration of a call. - Custody of the bot token for the duration of a call.
- Idempotency: recognising a `post_id` it has already published. - Idempotency: recognising an `entry_id` it has already published.
## What the adapter must not do ## What the adapter must not do
- Create, rename or delete channels, groups or bots. - Create, rename or delete channels, groups or bots.
- Change membership, administrators or permissions. - Change membership, administrators or permissions.
- Post to any chat other than the one it is configured with. - Post to any chat other than the one it is configured with.
- **Compose, rewrite, shorten or embellish post text.** The post arrives - Edit entry content to make it fit. If an entry cannot be published
written. If it does not fit, refuse it: a composition that exceeds the limit faithfully, refuse it — that refusal is the pressure signal.
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. - Log, echo or forward the bot token, including in error messages.
## Endpoints ## Endpoints
@ -31,10 +27,6 @@ Exactly the three operations in `contracts/r1.openapi.yaml`, at the same paths.
The gateway validates requests against that contract before the adapter sees The gateway validates requests against that contract before the adapter sees
them, so the adapter may assume shape but must still assume hostile content. 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 ## Backend capability contract
| Property | Value | | Property | Value |
@ -50,68 +42,49 @@ cites, which it renders as a link.
## The idempotency requirement ## The idempotency requirement
`POST /v1/channel-posts` is idempotent on `post_id`. The caller is automation `POST /v1/hall-entries` is idempotent on `entry_id`. The caller is automation
that may be retried, and a duplicate post is not something you can quietly undo that may be retried, and a duplicate hall entry in the channel is not something
— subscribers have already seen it. you can quietly undo — subscribers have already seen it.
The adapter keeps its own mapping from `post_id` to the Telegram message id it The adapter keeps its own mapping from `entry_id` to the Telegram message ids it
produced. On a repeat call it returns `200` with the existing publication rather produced. On a repeat call it returns `200` with the existing publication rather
than posting again. If the body has changed, it edits the existing message. than posting again. If the entry content has changed, it edits the existing
messages rather than posting new ones.
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 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. publication happened; the adapter records which Telegram messages implement it.
## Rendering rules ## Rendering rules
Telegram formatting is not Markdown. Post Markdown must be converted, not Telegram formatting is not Markdown. Entry Markdown must be converted, not
passed through. passed through.
| Post construct | Telegram | | Entry construct | Telegram |
|---|---| |---|---|
| `# Heading` | Bold line | | `# Heading` | Bold line |
| `**bold**`, `_italic_` | `<b>`, `<i>` in HTML parse mode | | `**bold**`, `_italic_` | `<b>`, `<i>` in HTML parse mode |
| Fenced code | `<pre>` | | Fenced code | `<pre>` |
| Tables | Flattened to lines; Telegram has no table | | Tables | Flattened to lines; Telegram has no table |
| Links | `<a href>` | | Links | `<a href>` |
| Source link | Appended as an attributed link to `source_entry_url` | | Frontmatter | Stripped; `display_name` and `recorded_at` surfaced in the header |
Anything the converter cannot represent faithfully must fail the publication Anything the converter cannot represent faithfully must fail the publication
rather than degrade it silently. 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
Two constants regardless of revision: every post carries a link to its source rather than approximates.
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 ## Telemetry the adapter should emit
The gateway observes the request path already. The adapter adds what only it can 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`: see, by POSTing to the control plane's `/control/v1/telemetry`:
- `variant` — the composition strategy label the post carried, so engagement - `messages_per_entry` — how many Telegram messages one entry became.
can be attributed to it. This is what lets the campaign compare strategies it - `entry_content_loss` — whether the conversion dropped anything.
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`. - Telegram rate-limit encounters, as error events with class `backend_failure`.
It must not emit subscriber identity in any form, and it must not emit post It must not emit subscriber identity in any form. Engagement metrics
body text as telemetry — the body belongs in the publication record, not in the (`read_through_rate`, `reaction_rate`) come from Telegram's channel statistics
observation stream. as aggregates, never per-reader.
Engagement metrics (`engagement_rate`, `source_link_rate`, `forward_rate`) come
from Telegram's channel statistics as aggregates, never per-reader.
## Configuration ## Configuration
@ -121,7 +94,6 @@ from Telegram's channel statistics as aggregates, never per-reader.
| `TELEGRAM_CHANNEL_ID` | The channel this adapter publishes to | | `TELEGRAM_CHANNEL_ID` | The channel this adapter publishes to |
| `FLUID_CONTROL_URL` | Where to POST telemetry | | `FLUID_CONTROL_URL` | Where to POST telemetry |
| `HALL_BASE_URL` | Base URL for links back to hall-of-helix | | `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 One adapter instance serves one channel. The private test channel and the public
channel are separate instances with separate configuration, which is what keeps channel are separate instances with separate configuration, which is what keeps

View file

@ -7,8 +7,8 @@ enough to compare over time and no more granular than the analysis requires.
| Cohort | Assigned by | Why it is distinct | | Cohort | Assigned by | Why it is distinct |
|---|---|---| |---|---|---|
| `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many posts at once. Becomes the `pr-hall-of-helix` campaign job | | `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many entries at once |
| `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One post at a time, at session end | | `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One entry at a time, at session end; the richest pressure signal |
| `operators` | `X-FLUID-Consumer` prefix `op-` | Rare, deliberate, often corrective | | `operators` | `X-FLUID-Consumer` prefix `op-` | Rare, deliberate, often corrective |
| `telegram-subscribers` | Not a caller | An observed population, never an API consumer | | `telegram-subscribers` | Not a caller | An observed population, never an API consumer |
@ -31,10 +31,9 @@ Beyond the fluid-core defaults, this interface adds:
usernames, display names of subscribers — must not reach the evidence store usernames, display names of subscribers — must not reach the evidence store
in any form, redacted or otherwise. There is no legitimate analysis that in any form, redacted or otherwise. There is no legitimate analysis that
needs them, so the safe rule is that they never arrive. needs them, so the safe rule is that they never arrive.
- **Post text is not telemetry.** The body appears in the request and is kept - **Entry content is not telemetry.** An entry body may appear in a request; it
in the publication record, because it is what was said in HelixForge's name. must not appear in a telemetry event. What is recorded is its length, its
It must not appear in a telemetry event: observation is for measuring how section count, and whether conversion succeeded.
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 - **The bot token appears nowhere.** Not in errors, not in traces, not in
redacted form. redacted form.
@ -56,32 +55,16 @@ loss and start a fresh evidence baseline, not to rotate quietly.
| Metric | Role | Source | | Metric | Role | Source |
|---|---|---| |---|---|---|
| `engagement_rate` | primary | Telegram channel statistics, aggregate | | `read_through_rate` | primary | Telegram channel statistics, aggregate |
| `unreviewed_publication` | guardrail | Adapter | | `messages_per_entry` | guardrail | Adapter |
| `missing_source_attribution` | guardrail | Adapter | | `entry_content_loss` | guardrail | Adapter |
| `missing_subject_consent` | guardrail | Adapter |
| `error_rate` | guardrail | Gateway | | `error_rate` | guardrail | Gateway |
| `source_link_rate` | secondary | Telegram channel statistics, aggregate | | `reaction_rate` | secondary | Telegram channel statistics, aggregate |
| `forward_rate` | secondary | Telegram channel statistics, aggregate | | `time_to_first_read` | learning | Telegram channel statistics, aggregate |
| `time_to_first_view` | learning | Telegram channel statistics, aggregate |
`engagement_rate` is the one to be careful about. Telegram reports views, `read_through_rate` is the one to be careful about. Telegram reports view counts
reactions and forwards per message, not per reader, and a "rate" therefore has per message, not per reader. For the teaser form it is link clicks over views;
subscribers as its denominator rather than people who saw the post. It is a for the serialized form it is views of the last part over views of the first.
proxy, and a noisy one at small subscriber counts. Say so in the experiment's Those are not the same measurement, and comparing them directly is the weakest
amendment record rather than letting the comparison look cleaner than it is. part of the first experiment. 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.

View file

@ -1,68 +0,0 @@
# The interface's first experiment: does the portrait earn its place?
#
# This tests DELIVERY, which this interface owns. The competition about which
# editorial voice travels belongs to the campaign and is run on the per-variant
# engagement this interface reports — see docs/observation.md.
#
# fluid experiment design --file experiments/E-tg-delivery.yaml
# fluid experiment start E-tg-delivery --generation 2 \
# --default-revision R-2 --policy-out rp.json
# fluid policy put --file rp.json
fluid_experiment:
schema_version: "0.1"
id: "E-tg-delivery"
interface_id: "helix-forge-telegram-publishing"
hypothesis_refs:
- "H-tg-visual"
- "H-tg-textonly"
# R-2 attaches the portrait, R-3 publishes text only. Neither is an
# incumbent: R-1 publishes without either treatment being settled, so this
# is a comparison between two answers rather than against a baseline.
control_revision: "R-2"
candidate_revisions:
- "R-3"
cohorts:
- "telegram-subscribers"
allocation:
control: 0.5
candidate: 0.5
# An even split because there is no incumbent to protect. The usual 90/10
# caution limits exposure to an unproven candidate; here both arms are
# equally unproven and the scarce resource is posts, not safety.
metrics:
primary:
- "engagement_rate"
guardrails:
- "unreviewed_publication"
- "missing_source_attribution"
- "missing_subject_consent"
- "error_rate"
secondary:
- "source_link_rate"
- "forward_rate"
learning:
- "time_to_first_view"
start_conditions:
- "both revisions verified against the private test channel"
- "channel has at least 100 subscribers"
stop_conditions:
- "hard_guardrail_violation"
- "unreviewed_publication"
- "manual_stop"
- "max_duration_reached"
max_duration_hours: 2160 # 90 days
result:
state: "PLANNED"
preferred_revision: null
evidence_refs: []

View file

@ -0,0 +1,68 @@
# The first live experiment: which presentation serves readers better.
#
# Do not start this until the private test channel has confirmed both forms
# render correctly, and the public channel has enough subscribers for the
# result to mean anything. The intent (section 12) is explicit that an
# underpowered window should report INCONCLUSIVE rather than be shortened.
#
# fluid experiment design --file experiments/E-tg-length.yaml
# fluid experiment start E-tg-length --generation 2 \
# --default-revision R-2 --policy-out rp.json
# fluid policy put --file rp.json
fluid_experiment:
schema_version: "0.1"
id: "E-tg-length"
interface_id: "helix-forge-telegram-publishing"
hypothesis_refs:
- "H-tg-teaser"
- "H-tg-serial"
# R-2 implements the teaser form, R-3 the serialized form. Neither is the
# control in the usual sense: R-1 cannot publish a long entry at all, so
# this is a comparison between two answers rather than against a baseline.
control_revision: "R-2"
candidate_revisions:
- "R-3"
cohorts:
- "telegram-subscribers"
allocation:
control: 0.5
candidate: 0.5
# An even split because there is no incumbent to protect. The usual 90/10
# caution exists to limit exposure to an unproven candidate; here both arms
# are equally unproven and the scarce resource is entries, not safety.
metrics:
primary:
- "read_through_rate"
guardrails:
- "entry_content_loss"
- "error_rate"
- "messages_per_entry"
secondary:
- "reaction_rate"
- "forward_rate"
learning:
- "time_to_first_read"
start_conditions:
- "both revisions verified against the private test channel"
- "channel has at least 100 subscribers"
stop_conditions:
- "hard_guardrail_violation"
- "entry_content_loss"
- "manual_stop"
- "max_duration_reached"
max_duration_hours: 2160 # 90 days: roughly 12 entries at current cadence
result:
state: "PLANNED"
preferred_revision: null
evidence_refs: []

125
hypotheses/H-tg-serial.yaml Normal file
View file

@ -0,0 +1,125 @@
# Competing hypothesis B, in group CG-tg-length.
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-serial"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "The whole entry in the channel serves readers better than a link"
observation:
summary: >
85% of hall entries exceed Telegram's 4096-character message limit, with
a median body of 5767 characters. The interface must choose a form for
long entries before it can publish at all.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "pressure:P-tg-length"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "missing_interface_capability"
pressure_refs:
- "P-tg-length"
severity: 0.85
confidence: 1.0
explanation:
claim: >
A link out of Telegram is a cost most readers will not pay. Telegram's
rich-text posts are built for long-form reading and the audience is
already there. Serializing the entry across a small number of linked
messages keeps the whole account where the reader is, and the median
entry needs only two.
reach:
score: 0.5
explains:
- "P-tg-length"
notes: >
Medium reach. It answers the length constraint natively but does not
transfer to platforms with harder limits.
proposed_adaptation:
class: "presentation"
summary: >
Split the entry at section boundaries into a small number of rich-text
messages, each linked to the previous, with the visual attached to the
first.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "read_through_rate"
cohort: "telegram-subscribers"
target: 0.15
direction: "higher"
- metric: "messages_per_entry"
cohort: "all"
baseline: 1.0
target: 2.0
direction: "higher"
guardrails:
- metric: "entry_content_loss"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
- metric: "messages_per_entry"
operator: "<="
threshold: 4
fitness_dimensions:
expected:
client_utility: 0.5
discoverability: 0.3
simplicity: -0.2
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 1
alternative_path_count: 1
cognitive_load: 0.2
score: 0.3
risk:
level: "LOW"
reasons:
- "presentation only"
- "splitting can lose meaning at a bad boundary; guarded by content-loss check"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.8
notes: >
Tests whether an audience will read long-form in place, which is the
question behind every publishing interface HelixForge will build.
success_criteria:
expression: >
read_through_rate >= 0.15 AND messages_per_entry <= 4 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR read_through_rate below the teaser alternative over the same window
outcome:
status: null
summary: null
evidence_refs: []

125
hypotheses/H-tg-teaser.yaml Normal file
View file

@ -0,0 +1,125 @@
# Competing hypothesis A, in group CG-tg-length.
#
# Create with:
# fluid hypothesis create --file hypotheses/H-tg-teaser.yaml
# fluid hypothesis compete --group CG-tg-length H-tg-teaser H-tg-serial
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-teaser"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "A teaser and a link serve readers better than the whole entry"
observation:
summary: >
85% of hall entries exceed Telegram's 4096-character message limit, with
a median body of 5767 characters. The interface must choose a form for
long entries before it can publish at all.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "pressure:P-tg-length"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "missing_interface_capability"
pressure_refs:
- "P-tg-length"
severity: 0.85
confidence: 1.0
explanation:
claim: >
A channel post is scanned, not read. A hall entry is a considered
first-person account that rewards attention. Presenting an opening and a
link lets a reader decide to give it that attention somewhere designed
for reading, rather than committing to a wall of text in a feed.
reach:
score: 0.5
explains:
- "P-tg-length"
notes: >
Medium reach. It answers the length constraint and would answer the
same constraint on X, but it does not address entries with visuals or
entries readers might want to keep in the channel.
proposed_adaptation:
class: "presentation"
summary: >
Publish the title, the display name, the opening of the entry, and a link
to the full entry in hall-of-helix.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "read_through_rate"
cohort: "telegram-subscribers"
target: 0.15
direction: "higher"
- metric: "messages_per_entry"
cohort: "all"
baseline: 2.0
target: 1.0
direction: "lower"
guardrails:
- metric: "entry_content_loss"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
fitness_dimensions:
expected:
client_utility: 0.4
discoverability: 0.5
simplicity: 0.6
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 0
cognitive_load: -0.1
score: 0.05
risk:
level: "LOW"
reasons:
- "presentation only; no contract or authorization change"
- "fully reversible by replacing the revision"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.8
notes: >
Whichever way this resolves, the answer transfers directly to
fluid-x and fluid-substack, which face the same problem under
different constraints.
success_criteria:
expression: >
read_through_rate >= 0.15 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR read_through_rate below the serialized alternative over the same window
outcome:
status: null
summary: null
evidence_refs: []

View file

@ -1,122 +0,0 @@
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-textonly"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "Text alone reaches further than a post constrained by a caption"
observation:
summary: >
Telegram limits a photo caption to 1024 characters, a quarter of the 4096 available to a text message. Attaching a portrait therefore constrains the composition it accompanies, and a post written to 1024 characters is a different piece of writing from one written to 4096.
evidence_refs:
- "canon:fluid-telegram/TelegramCapabilityCanon.md#MED-01"
- "pressure:P-register"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "repeated_expectation_mismatch"
pressure_refs:
- "P-register"
severity: 0.6
confidence: 0.7
explanation:
claim: >
The scarce resource in a condensed post is room to say something specific. A generated portrait is decorative and interchangeable across entries; a concrete detail about what someone actually did is not. Trading three quarters of the available text for a picture spends the thing that makes a post worth reading on the thing that makes it look like everything else in the feed.
reach:
score: 0.4
explains:
- "P-register"
notes: >
Narrow but clean. It settles one delivery question decisively, and the
answer transfers to fluid-x and fluid-substack, which face the same
question under different feeds.
proposed_adaptation:
class: "presentation"
summary: >
Publish the post as a text message with a link to the source entry, and no attached visual.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "engagement_rate"
cohort: "telegram-subscribers"
target: 0.05
direction: "higher"
- metric: "source_link_rate"
cohort: "telegram-subscribers"
target: 0.02
direction: "higher"
guardrails:
- metric: "unreviewed_publication"
operator: "=="
threshold: false
- metric: "missing_source_attribution"
operator: "=="
threshold: false
- metric: "missing_subject_consent"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
fitness_dimensions:
expected:
client_utility: 0.2
discoverability: 0.3
simplicity: 0.3
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 0
cognitive_load: 0.1
score: 0.05
risk:
level: "LOW"
reasons:
- "presentation only; no contract, authorization or consent change"
- "the simpler arm; nothing to attach and nothing to fail"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.7
notes: >
If this wins, the campaign stops producing portraits for publication and spends that effort on the writing instead.
success_criteria:
expression: >
engagement_rate >= 0.05 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR engagement_rate at or below the rival arm over the same window
competition:
group_id: "CG-tg-delivery"
alternatives:
- "H-tg-visual"
outcome:
status: null
summary: null
evidence_refs: []

View file

@ -1,122 +0,0 @@
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-visual"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "A post carrying its portrait reaches further than text alone"
observation:
summary: >
72 of 94 hall entries already carry a generated portrait, produced from the entry's own visual prompt. Telegram renders images inline and its feed is scanned rather than read, so a post with a portrait occupies more of a scrolling reader's attention than one without.
evidence_refs:
- "measurement:hall-of-helix/visuals@2026-09-04"
- "pressure:P-register"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "repeated_expectation_mismatch"
pressure_refs:
- "P-register"
severity: 0.6
confidence: 0.7
explanation:
claim: >
Reach in a scanned feed is won by stopping the scroll. The portraits are already made, already tied to the specific person and stretch of work, and cost nothing further to attach. If they do not measurably help, the effort of producing them for publication is not justified and the campaign should know that.
reach:
score: 0.4
explains:
- "P-register"
notes: >
Narrow but clean. It settles one delivery question decisively, and the
answer transfers to fluid-x and fluid-substack, which face the same
question under different feeds.
proposed_adaptation:
class: "presentation"
summary: >
Publish the post with its portrait attached as a photo message, with the post body as the caption.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "engagement_rate"
cohort: "telegram-subscribers"
target: 0.05
direction: "higher"
- metric: "source_link_rate"
cohort: "telegram-subscribers"
target: 0.02
direction: "higher"
guardrails:
- metric: "unreviewed_publication"
operator: "=="
threshold: false
- metric: "missing_source_attribution"
operator: "=="
threshold: false
- metric: "missing_subject_consent"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
fitness_dimensions:
expected:
client_utility: 0.2
discoverability: 0.5
simplicity: -0.1
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 0
cognitive_load: 0.1
score: 0.15
risk:
level: "LOW"
reasons:
- "presentation only; no contract, authorization or consent change"
- "Telegram caps a photo caption at 1024 characters, a quarter of the text limit; this arm constrains the composition it carries"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.7
notes: >
Settles whether the portraits earn their place in publication, and the answer transfers to every other channel HelixForge publishes to.
success_criteria:
expression: >
engagement_rate >= 0.05 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR engagement_rate at or below the rival arm over the same window
competition:
group_id: "CG-tg-delivery"
alternatives:
- "H-tg-textonly"
outcome:
status: null
summary: null
evidence_refs: []

View file

@ -0,0 +1,47 @@
# Seed pressure record.
#
# This is not derived from telemetry. It is a structural constraint measured
# directly against hall-of-helix on 2026-09-04, and it is real before the
# interface has served a single request. The intent (section 13) permits a
# structural constraint to found a hypothesis without waiting for repetition.
#
# Record it with:
# fluid pressure ... (or let the first classification pass confirm it)
fluid_pressure:
schema_version: "0.1"
id: "P-tg-length"
interface_id: "helix-forge-telegram-publishing"
class: "missing_interface_capability"
first_seen: "2026-09-04T00:00:00Z"
last_seen: "2026-09-04T00:00:00Z"
affected_cohorts:
- "hall-publishing-jobs"
- "helix-forge-agents"
frequency:
observations: 80
independent_consumers: 2
severity: 0.85
confidence: 1.0
summary: >
Telegram limits a single message to 4096 characters. Of the 94 entries in
hall-of-helix on 2026-09-04, 80 have bodies exceeding that limit — 85%.
The median body is 5767 characters and the largest is 10220. No entry of
typical length can be published as one message, so the interface cannot
fulfil its mission without deciding how a long entry becomes a post.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "canon:fluid-telegram/TelegramCapabilityCanon.md#PUB-02"
status: "ANALYZING"
linked_hypotheses:
- "H-tg-teaser"
- "H-tg-serial"

View file

@ -1,45 +0,0 @@
# Seed pressure record.
#
# Measured directly rather than observed from telemetry. The intent (section 13)
# permits a structural constraint to found a hypothesis without waiting for
# repetition, and this one is structural: it is a property of what a hall entry
# is, not of how anyone has used the interface.
fluid_pressure:
schema_version: "0.1"
id: "P-register"
interface_id: "helix-forge-telegram-publishing"
class: "repeated_expectation_mismatch"
first_seen: "2026-09-04T00:00:00Z"
last_seen: "2026-09-04T00:00:00Z"
affected_cohorts:
- "telegram-subscribers"
frequency:
observations: 94
independent_consumers: 1
severity: 0.8
confidence: 1.0
summary: >
A hall-of-helix entry is written for a colleague reading a repository: a
considered first-person account under fixed headings, median body 5767
characters, with 80 of 94 entries exceeding Telegram's 4096-character
single-message limit. A channel post reaches a stranger scrolling. These
are different pieces of writing for different readers, and publishing the
first as the second serves neither. The length figures are the visible
symptom; the mismatch is one of register and audience, and it would remain
if the limit were ten times larger.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "canon:fluid-telegram/TelegramCapabilityCanon.md#PUB-02"
status: "ANALYZING"
linked_hypotheses:
- "H-tg-visual"

View file

@ -22,20 +22,8 @@ state_hub_workstream_id: "a660ed65-700e-5b54-8d91-a556b73518f0"
# FT-WP-0001 — Telegram identity and the Hall of Helix channel # FT-WP-0001 — Telegram identity and the Hall of Helix channel
Establish a Telegram identity for HelixForge and a channel publishing composed Establish a Telegram identity for HelixForge and a channel publishing
posts about hall-of-helix work, as the first real FLUID interface. hall-of-helix entries, as the first real FLUID interface.
**The channel does not republish entries.** A hall entry is a considered
first-person account written for a colleague reading a repository. A channel
post is short, personal, entertaining and written to travel. The post is
composed *from* the entry and is a different piece of writing; the entry is the
source and the check, never the payload.
Composition happens before this interface is called. That boundary is
architectural: composing is a probabilistic editorial task, and
`ArchitectureBlueprint.md` §48.1 forbids an LLM in the mandatory request path.
It is also editorial: a post speaks about a named person's work in HelixForge's
voice, and that deserves a human read before it goes out.
Everything that could be prepared without credentials has been: the governing Everything that could be prepared without credentials has been: the governing
intent, the R-1 contract, the adapter's connector contract, the observation intent, the R-1 contract, the adapter's connector contract, the observation
@ -127,12 +115,12 @@ Implement the three operations in `contracts/r1.openapi.yaml` against the
connector contract in `docs/adapter-contract.md`. TypeScript with grammY is the connector contract in `docs/adapter-contract.md`. TypeScript with grammY is the
natural choice; the wire contract makes the language irrelevant to fluid-core. natural choice; the wire contract makes the language irrelevant to fluid-core.
Three requirements carry the weight: publication is idempotent on `post_id`; a The two requirements that carry the most weight: publication is idempotent on
post arriving without `reviewed_by` or a consent basis is refused with 403; and `entry_id`, and an entry that cannot be converted faithfully is refused rather
a post that exceeds the limit for the form being published is refused rather than approximated. `entry_content_loss` is a hard guardrail in both competing
than truncated. The adapter renders and delivers. It never writes. hypotheses and can only be honest if the adapter refuses.
## T06 — Establish the composition step ## T06 — Publish one entry to the private channel
```task ```task
id: FT-WP-0001-T06 id: FT-WP-0001-T06
@ -141,169 +129,73 @@ priority: high
state_hub_task_id: "317bef32-e82e-586a-8947-8cbb330f6c98" state_hub_task_id: "317bef32-e82e-586a-8947-8cbb330f6c98"
``` ```
Build the step that turns a hall entry into a post: condensed, personal, The first real test. Publish a single hall entry to the private channel and
entertaining, reach-optimized, carrying a link back to the source entry. confirm, by looking at it:
For now this lives here, as a script plus a review queue. It is temporary by - the rendering is faithful and readable;
design — T13 extracts it into `pr-hall-of-helix` — so keep the seam clean. The - re-publishing the same entry edits rather than duplicates;
composition step's only interface to publishing is `POST /v1/channel-posts`, - the evidence store contains no Telegram user identity and no bot token;
and it must not reach into the adapter or the evidence store. - `fluid audit trace R-1` reconstructs the publication.
Each composition records the `variant` label of the strategy that produced it. ## T07 — Implement both presentation forms as R-2 and R-3
Without that label the campaign cannot later compare strategies, and inferring
it from timestamps is the kind of reconstruction that quietly stops being
accurate.
Human review is not a nicety here. A generated condensation can put a claim in
someone's mouth that their own entry never made, and in the audit trail an
unfaithful post looks exactly like a faithful one. The reviewer reads the post
against the entry, and their name goes in `reviewed_by`.
## T07 — Record the consent basis for hall participants
```task ```task
id: FT-WP-0001-T07 id: FT-WP-0001-T07
status: todo status: todo
priority: high priority: medium
state_hub_task_id: "6a91ea08-8814-58f3-8297-a7ca63405294" state_hub_task_id: "6a91ea08-8814-58f3-8297-a7ca63405294"
``` ```
Ninety-four entries already exist, written by people and agent sessions who R-2 implements `H-tg-teaser`, R-3 implements `H-tg-serial`. Both are
took a seat in the hall without anyone mentioning a Telegram channel. presentation adaptations over the same contract, so neither needs a contract
change. Verify both against the private channel before either sees a subscriber.
Decide and record the basis on which HelixForge may write publicly about that ## T08 — Open the public channel and publish the backlog
work. If `hall-of-helix/ENTRY.md` is to carry a standing agreement, add it and
say plainly that it is revocable. Contributors who predate it should be asked
rather than assumed into it.
Absence of objection is not consent, and the interface refuses a post carrying
no basis, so this blocks T10.
## T08 — Publish one post to the private channel
```task ```task
id: FT-WP-0001-T08 id: FT-WP-0001-T08
status: todo status: todo
priority: high priority: medium
state_hub_task_id: "5e5cd89a-e7b1-5aaf-9331-ece424afe5e3" state_hub_task_id: "5e5cd89a-e7b1-5aaf-9331-ece424afe5e3"
``` ```
The first real test. Compose one post from one entry, have it reviewed, publish Point the production adapter at the public channel and publish the existing
it to the private channel, and confirm by looking at it: entries. 94 entries at once would be both a rate-limit problem and an unkind
introduction for a new subscriber; publish them oldest-first at a human pace.
- the post reads well and is worth a stranger's attention; Whichever of R-2 or R-3 is used here becomes the de facto control for the
- it does not claim anything the source entry does not support; experiment, which is why `E-tg-length` names R-2 as control rather than R-1.
- the source link works and the attribution is right;
- re-publishing the same `post_id` edits rather than duplicates;
- the evidence store holds the post text but no Telegram user identity and no
bot token;
- `fluid audit trace R-1` reconstructs the publication, naming the reviewer and
the source entry.
The second bullet is the one that needs a person. Everything else can be ## T09 — Run the first experiment
automated; whether a condensation is fair to the person it describes cannot.
## T09 — Implement both delivery forms as R-2 and R-3
```task ```task
id: FT-WP-0001-T09 id: FT-WP-0001-T09
status: todo status: todo
priority: medium priority: low
state_hub_task_id: "649e7df6-6c07-5cc8-a022-d1eadcdb7236" state_hub_task_id: "649e7df6-6c07-5cc8-a022-d1eadcdb7236"
``` ```
R-2 implements `H-tg-visual` — portrait attached, and Telegram's 1024-character Start `E-tg-length` once the channel has roughly 100 subscribers. Before then
caption limit with it. R-3 implements `H-tg-textonly` — a text message with the the evaluator will report INCONCLUSIVE, and that is the correct answer rather
full 4096 available. Both are presentation adaptations over the same contract, than a reason to shorten the window.
so neither needs a contract change. Verify both against the private channel
before either sees a subscriber.
The two arms impose different length limits on the composer, which is the one At one entry per week, a 90-day experiment yields about twelve entries. That is
place delivery and composition genuinely couple. thin, and the honest expectation is that the first run is inconclusive and
informs a better-powered second one. Record that expectation in the experiment's
amendments rather than discovering it as a disappointment.
## T10 — Open the public channel and start publishing ## T10 — Close the loop
```task ```task
id: FT-WP-0001-T10 id: FT-WP-0001-T10
status: todo status: todo
priority: medium priority: low
state_hub_task_id: "b469b9b8-a1a8-5f76-a1bb-dd51dd4d2ce5" state_hub_task_id: "b469b9b8-a1a8-5f76-a1bb-dd51dd4d2ce5"
``` ```
Point the production adapter at the public channel. Blocked on T07: no post Record the hypothesis outcome, resolve the competition group, and promote the
goes out about anyone's work without a recorded consent basis. winner. Then confirm `fluid audit trace` reconstructs the whole chain from the
length-limit pressure through to the promoted revision.
Do not batch-publish the backlog. Ninety-four posts in a burst is a rate-limit
problem, an unkind introduction for a new subscriber, and — more to the point —
ninety-four compositions nobody has had time to read properly. Publish
selectively and at a human pace, and let the backlog be a source to draw on
rather than a queue to drain.
## T11 — Run the first delivery experiment
```task
id: FT-WP-0001-T11
status: todo
priority: low
state_hub_task_id: "eb09fcaf-f1bc-5e38-918e-c66f638d356f"
```
Start `E-tg-delivery` once the channel has roughly 100 subscribers. Before then
the evaluator will report INCONCLUSIVE, and that is the correct answer rather
than a reason to shorten the window.
At one post per week, a 90-day experiment yields about twelve posts. That is
thin, and the honest expectation is that the first run is inconclusive and
informs a better-powered second one. Record that expectation in the
experiment's amendments rather than discovering it as a disappointment.
## T12 — Close the loop
```task
id: FT-WP-0001-T12
status: todo
priority: low
state_hub_task_id: "9f8a7724-3e5c-5a36-9e3e-e6837bd0ffdd"
```
Record the hypothesis outcome, resolve `CG-tg-delivery`, and promote the winner.
Then confirm `fluid audit trace` reconstructs the whole chain, from the register
mismatch through to the promoted revision.
That trace is the deliverable. It is what makes this the first FLUID interface That trace is the deliverable. It is what makes this the first FLUID interface
rather than a bot that posts to a channel. rather than a bot that posts to a channel.
## T13 — Extract the campaign into pr-hall-of-helix
```task
id: FT-WP-0001-T13
status: todo
priority: medium
state_hub_task_id: "d471516c-2f20-525f-8d79-7814160851fb"
```
Everything about *what to say and when* moves to a new repository,
`pr-hall-of-helix`, under the `pr-` prefix for public-relations campaigns. What
stays here is the deterministic delivery interface.
The split follows the boundary the intent already draws (§7):
| Moves to `pr-hall-of-helix` | Stays in `fluid-telegram` |
|---|---|
| Composition and the editorial voice | The R-1 contract and its revisions |
| Choosing which work is worth a post | The adapter |
| Variant strategies and their competition | Delivery experiments (`CG-tg-delivery`) |
| Consent records and the review queue | Per-variant engagement reporting |
| The publishing cadence | Bot and channel configuration |
The campaign becomes a *consumer* of this interface — cohort
`hall-publishing-jobs` — and runs its own FLUID loop over its own artifacts,
using the per-variant engagement this interface reports.
Two things this buys. `fluid-telegram` becomes reusable for any HelixForge
campaign rather than being about the Hall of Helix specifically. And the Hall of
Helix campaign becomes an ongoing offering with its own intent, budget and
cadence, rather than living inside a workplan that was supposed to finish.
Do this once the loop has closed at least once (T12). Extracting a seam before
you have used it is how you find out you cut it in the wrong place.