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:
parent
55363905bc
commit
6834c5c3e7
2 changed files with 224 additions and 0 deletions
51
conformance/handover_test.go
Normal file
51
conformance/handover_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
173
docs/handover/fluid-telegram.md
Normal file
173
docs/handover/fluid-telegram.md
Normal 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
|
||||
revision–experiment–fitness 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue