diff --git a/conformance/handover_test.go b/conformance/handover_test.go new file mode 100644 index 0000000..74240d7 --- /dev/null +++ b/conformance/handover_test.go @@ -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)) + } +} diff --git a/docs/handover/fluid-telegram.md b/docs/handover/fluid-telegram.md new file mode 100644 index 0000000..8f0c2bd --- /dev/null +++ b/docs/handover/fluid-telegram.md @@ -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.