Add the fluid-telegram handover package

Completes FLUID-WP-0008's deliverable in this repository. The handover
records the measured pressure that motivates the interface -- 80 of 94
hall entries exceed Telegram's 4096-character limit -- and names the
three places this workload will strain the framework honestly, so the
strain reads as information rather than as a surprise.

A conformance test asserts the handover's contract compiles with
fluid-core's own validator and stays within the intent's complexity
budget, so a change on either side that broke the handover fails CI
rather than failing at the first publication.

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:
tegwick 2026-09-04 11:07:51 +02:00
parent 55363905bc
commit 6834c5c3e7
2 changed files with 224 additions and 0 deletions

View file

@ -0,0 +1,51 @@
package conformance
import (
"os"
"testing"
"github.com/tegwick/fluid-core/internal/validate"
)
// TestTelegramHandoverContractCompiles checks that the contract shipped in the
// fluid-telegram handover is one this gateway can actually enforce.
//
// A handover that shipped a contract the framework cannot parse would fail at
// the first publication, after the bot and channel had already been created.
// The check is skipped when the sibling repository is absent, so CI in a bare
// checkout still passes.
func TestTelegramHandoverContractCompiles(t *testing.T) {
const path = "../../fluid-telegram/contracts/r1.openapi.yaml"
raw, err := os.ReadFile(path)
if err != nil {
t.Skipf("fluid-telegram not checked out beside fluid-core: %v", err)
}
c, err := validate.ParseOpenAPI(raw)
if err != nil {
t.Fatalf("the handover contract does not compile: %v", err)
}
ops := c.Operations()
want := map[string]bool{
"GET /v1/hall-entries": false,
"POST /v1/hall-entries": false,
"GET /v1/hall-entries/{entry_id}": false,
}
for _, op := range ops {
if _, tracked := want[op]; tracked {
want[op] = true
}
}
for op, found := range want {
if !found {
t.Errorf("the contract does not declare %s; it has %v", op, ops)
}
}
// The interface's complexity budget allows a maximum of 8 operations.
if len(ops) > 8 {
t.Errorf("the contract declares %d operations, over the intent's budget of 8", len(ops))
}
}

View file

@ -0,0 +1,173 @@
# Handover — fluid-telegram
**From:** fluid-core (FLUID-WP-0008)
**To:** fluid-telegram (FT-WP-0001)
**Date:** 2026-09-04
The first real FLUID interface: a Telegram identity for HelixForge, and a
channel publishing hall-of-helix entries.
---
## What this is
fluid-core is finished through Blueprint Phase C. It can run the complete
revisionexperimentfitness loop, and `TestFirstVerticalSlice` proves it with no
human steps. What it has never done is carry a real workload.
`helix-forge-telegram-publishing` is that workload. It was chosen because the
pressure it faces is real, measurable and unavoidable rather than contrived.
---
## The pressure, measured
Telegram limits a single message to 4096 characters. Measured against
`hall-of-helix` on 2026-09-04:
| | |
|---|---|
| Entries | 94 |
| Bodies over 4096 characters | 80 (85%) |
| Median body | 5767 characters |
| Largest body | 10220 characters |
| Median parts if split at the limit | 2 |
No entry of typical length can be published as one message. The interface cannot
fulfil its mission without deciding how a long entry becomes a post, and that
decision has at least two defensible answers.
This is a better first pressure than anything a fixture could stage. It is
structural rather than behavioural, so it is real before the interface serves a
single request — and `InterfaceEvolutionIntent.md` §13 permits a structural
constraint to found a hypothesis without waiting for repetition.
---
## What is delivered
Everything that could be prepared without credentials.
| Artifact | Location |
|---|---|
| Governing intent | `fluid-telegram/InterfaceEvolutionIntent.md` |
| R-1 contract | `fluid-telegram/contracts/r1.openapi.yaml` |
| R-1 descriptor | `fluid-telegram/revisions/R-1.yaml` |
| Seed pressure record | `fluid-telegram/pressure/P-length-limit.yaml` |
| Competing hypotheses | `fluid-telegram/hypotheses/H-tg-{teaser,serial}.yaml` |
| First experiment | `fluid-telegram/experiments/E-tg-length.yaml` |
| Adapter connector contract | `fluid-telegram/docs/adapter-contract.md` |
| Cohorts, telemetry, redaction | `fluid-telegram/docs/observation.md` |
| Runbook | `fluid-telegram/docs/runbook.md` |
| Work | `fluid-telegram/workplans/FT-WP-0001-*.md` |
All four seed records validate against `fluid-core/schemas/`, and the R-1
contract compiles with fluid-core's own validator —
`conformance/handover_test.go` asserts both, so a change on either side that
broke the handover would fail CI rather than fail at the first publication.
---
## The competition
The length constraint has two defensible answers, and choosing between them by
argument is exactly the habit FLUID exists to break.
**H-tg-teaser** — publish the title, the author, an opening, and a link back to
the hall. *A channel post is scanned, not read; let the reader choose to give
the entry proper attention somewhere designed for it.*
**H-tg-serial** — split the entry at section boundaries into a small number of
linked rich-text messages. *A link out of Telegram is a cost most readers will
not pay; the audience is already here.*
Both are presentation adaptations, which Blueprint §37 identifies as the safest
and highest-value place to start: deterministic behaviour preserved, low
compatibility risk, rich adoption data.
Neither is obviously right. That is the point.
---
## Where the framework will be tested honestly
Three places where this workload will strain fluid-core, named now so the strain
is recognised as information rather than as a surprise.
**The experiment will be underpowered, and should say so.** At roughly one entry
per week, a 90-day experiment yields about twelve entries. The evaluator will
report INCONCLUSIVE, and that is the correct answer. The framework was built to
refuse a verdict it has not earned; the first live experiment is where that
refusal will be inconvenient rather than admirable. Do not shorten the window to
force a result.
**`read_through_rate` is not one metric.** Telegram reports views per message,
not per reader. For the teaser form the measure is link clicks over views; for
the serialized form it is views of the last part over views of the first. Those
are different measurements wearing one name, and comparing them directly is the
weakest part of the first experiment. It belongs in the experiment's amendment
record rather than hidden behind a shared label.
**Task grouping will overstate repeat counts.** The measurer buckets requests by
consumer and elapsed time when no chain id is supplied, so a burst of
publications will read as one task. The direction of any measurement will be
right and the magnitude will not. Supplying chain ids from the publishing job is
the fix, and it is cheap.
---
## Human steps
Four things need a person; nothing in the framework can or should do them.
1. **Register the bot** through BotFather, and write the token straight to
OpenBao.
2. **Create the private test channel first**, then the public one. Grant the bot
**Post Messages** and nothing else — the intent forbids it from using a wider
right, so granting one would create a permission the system may not exercise.
3. **Generate the pseudonymization salt** and store it beside the token. It must
never change.
4. **Look at the first publication** in the private channel before anything
reaches a subscriber. No automated check substitutes for someone reading the
post and deciding it is faithful.
---
## The boundary that matters most
`InterfaceEvolutionIntent.md` §7 forbids the Daimon from editing a hall entry to
make it publishable, and §14 sets every generate, experiment and promote
authority to `no`.
A hall entry is a first-person account someone wrote about their own work. The
authority to publish it comes from their having written it. An interface that
trimmed an entry to fit a platform limit would be quietly rewriting what someone
said about themselves in order to make a constraint go away — and it would look,
in the audit trail, exactly like a successful publication.
That is why `entry_content_loss` is a hard guardrail in both hypotheses and why
the adapter must refuse rather than approximate. The framework can measure
whether readers preferred a teaser or a serialization. It cannot be allowed to
decide that a shorter entry would have performed better.
---
## Verification, in order
```bash
export FLUID_INTERFACE=helix-forge-telegram-publishing
fluid intent put --version IEI-1 --file InterfaceEvolutionIntent.md --activate
fluid revision publish --file revisions/R-1.yaml --key-file "$FLUID_SIGNING_KEY" \
--adaptation-classes presentation --approved-by "$USER" --traffic-share 1.0
# then, against the private channel only:
# publish one entry
# publish it again; confirm one post, edited
# confirm no Telegram identity and no token in the evidence store
fluid audit trace R-1
```
The last command is the deliverable. A trace running from the length-limit
pressure through a hypothesis, an experiment and a promotion is what makes this
the first FLUID interface rather than a bot that posts to a channel.