Some checks failed
ci / build (push) Has been cancelled
Completes FLUID-WP-0007. The seven minimal-conformance requirements and the mechanically checkable architectural invariants are asserted as tests rather than claimed in a README, because a conformance claim nobody re-checks is one that quietly stops being true. Only the checkable subset of the invariants is asserted; pretending a test can settle the rest would be worse than leaving them to review. TestFirstVerticalSlice runs all eleven steps of Blueprint 50 with no human steps: two revisions, explicit routing, telemetry, a cohort dimension, detected pressure, a hypothesis, a candidate, a 90/10 experiment, fitness comparison, promotion, and a complete audit trail. Requests per completed task fall from 5.65 to 1.00 against a 1.20 target. A companion test runs the loop twice and requires the same verdict, since a loop whose conclusion depended on run order would be measuring the harness rather than the interface. The failure-containment matrix covers Blueprint 34 directly: the data plane keeps serving with the evidence store closed, with telemetry wedged against a sink that never returns, after a failed build, after an experiment rollback, and with the adaptive concurrency limit saturated. Fixes a real bug the suite exposed. Drain closed the emitter outright, so every request after the first flush emitted into a dead emitter and was silently lost -- the kind of fault that makes a later measurement quietly wrong rather than loudly broken. Emitter.Flush now waits for delivery without stopping it. 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
137 lines
5.3 KiB
Markdown
137 lines
5.3 KiB
Markdown
# Putting an existing API behind fluid-core
|
|
|
|
fluid-core attaches **out of process** (ADR-0002). Your API contributes no code,
|
|
imports no library, and may be written in any stack. Integration is over a wire
|
|
contract, so the fact that fluid-core is written in Go is not something you need
|
|
to care about.
|
|
|
|
## What you need
|
|
|
|
1. **A running service.** Anything that speaks HTTP. This becomes an *adapter*.
|
|
2. **A contract.** OpenAPI 3.1 today; the validator interface admits others.
|
|
3. **An interface evolution intent.** The governance document that bounds how
|
|
the interface may change.
|
|
|
|
Nothing else. In particular you do not need a Kubernetes cluster, a message
|
|
broker, or Postgres — SQLite is the default evidence store.
|
|
|
|
## The five minutes version
|
|
|
|
```bash
|
|
export FLUID_INTERFACE=my-api
|
|
export FLUID_STORE=$PWD/fluid.db
|
|
|
|
# 1. Record the intent that governs this interface.
|
|
fluid intent put --version IEI-1 --file InterfaceEvolutionIntent.md --activate
|
|
|
|
# 2. Publish a revision pointing at your existing service.
|
|
fluid revision publish --file r1.yaml --ephemeral-key \
|
|
--adaptation-classes presentation --approved-by "$USER" --traffic-share 1.0
|
|
|
|
# 3. Route traffic to it.
|
|
fluid policy put --file routing-policy.yaml
|
|
```
|
|
|
|
A revision descriptor is small:
|
|
|
|
```yaml
|
|
revision:
|
|
schema_version: "0.1"
|
|
id: "R-1"
|
|
interface: "my-api"
|
|
state: "stable"
|
|
contract:
|
|
type: "openapi"
|
|
digest: "sha256:<digest of your OpenAPI document>"
|
|
runtime:
|
|
upstream: "http://my-existing-service:8080" # your service, unchanged
|
|
timeout_ms: 5000
|
|
intent:
|
|
version: "IEI-1"
|
|
policy:
|
|
compatibility: "additive"
|
|
security_check: "passed"
|
|
```
|
|
|
|
`runtime.upstream` is the whole integration. Everything else is metadata about
|
|
what that upstream is allowed to be.
|
|
|
|
## What you get immediately
|
|
|
|
- **Revision identity.** Every response names the revision that served it.
|
|
- **Telemetry.** Requests, errors, call sequences and adoption, redacted before
|
|
storage.
|
|
- **Contract enforcement.** Undeclared paths, methods and fields are refused
|
|
rather than proxied.
|
|
- **An audit trail.** `fluid audit trace R-1` reconstructs how a revision came
|
|
to exist and what happened to it.
|
|
|
|
That is FLUID-0 (Instrumented) conformance, and it needs no AI and no change to
|
|
your service.
|
|
|
|
## What you should decide deliberately
|
|
|
|
**The pseudonymization salt.** Consumer identities are pseudonymized with HMAC.
|
|
The salt must be stable for the life of the interface — rotating it makes the
|
|
same consumer look like a new one and breaks every longitudinal comparison.
|
|
Store it where you store secrets, not in the repository.
|
|
|
|
**The authority mode.** Your intent document declares one, `FLUID-0` through
|
|
`FLUID-6`. This is a statement about authority, not maturity: a high-assurance
|
|
interface may deliberately stay at FLUID-2 forever. The unfilled template is
|
|
refused rather than defaulted, precisely so nobody inherits a permissive mode by
|
|
accident.
|
|
|
|
**Your complexity budget.** `policy.DefaultLimits()` permits only presentation
|
|
and implementation adaptations, refuses breaking changes, and caps exposure at
|
|
25%. If your interface needs more, say so explicitly in the intent rather than
|
|
widening the default.
|
|
|
|
## Adding a second revision
|
|
|
|
This is where FLUID earns its keep. Publish a second revision pointing at a
|
|
second adapter — a different build, a different service, a different language —
|
|
and let the experiment controller split traffic between them:
|
|
|
|
```bash
|
|
fluid revision publish --file r2.yaml --ephemeral-key ...
|
|
fluid experiment design --file experiment.yaml
|
|
fluid experiment start E-1 --generation 2 --default-revision R-1 --policy-out rp.json
|
|
fluid policy put --file rp.json
|
|
```
|
|
|
|
The controller does not touch traffic. It emits a routing policy for you to
|
|
install, which is what makes an experiment interruptible: stopping one replaces
|
|
a document rather than unwinding anything.
|
|
|
|
## Signing
|
|
|
|
`--ephemeral-key` is for development. It generates a throwaway key and warns
|
|
that revisions signed with it will not verify after a restart, which is exactly
|
|
what you want to hear before it happens in production.
|
|
|
|
In production, generate a key pair, keep the private half in your secret store,
|
|
and configure the gateway's registry with the public half. The router accepts
|
|
only descriptors that verify — an unsigned or tampered descriptor is refused,
|
|
including one that was signed and then edited.
|
|
|
|
## What fluid-core will not do for you
|
|
|
|
- **It will not modify your backend.** Where a candidate needs capability the
|
|
backend does not have, FLUID emits a structured requirement and stops. The
|
|
backend owner accepts, plans, or declares it out of scope.
|
|
- **It will not decide what "better" means.** There is no universal fitness
|
|
scalar. You declare primary metrics and guardrails; the evaluator compares
|
|
against what you declared and refuses to infer criteria from the data.
|
|
- **It will not promote on thin evidence.** A comparison below the sample floor
|
|
is inconclusive, not successful. You can override that with an acknowledged
|
|
flag; the override is recorded as one.
|
|
|
|
## Reference
|
|
|
|
- `examples/echo-interface/` — the two-revision fixture the conformance suite
|
|
drives, and the smallest complete example.
|
|
- `conformance/suite/` — every conformance requirement asserted as a test.
|
|
`TestFirstVerticalSlice` is the whole loop in one function.
|
|
- `spec/` — the normative documents. When this guide and the spec disagree, the
|
|
spec is right and this guide is a bug.
|