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
5.3 KiB
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
- A running service. Anything that speaks HTTP. This becomes an adapter.
- A contract. OpenAPI 3.1 today; the validator interface admits others.
- 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
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:
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-1reconstructs 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:
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.TestFirstVerticalSliceis 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.